Compare commits
5 Commits
1b34b0e974
...
8432bbb986
| Author | SHA1 | Date | |
|---|---|---|---|
| 8432bbb986 | |||
| d40ea1e537 | |||
| d88bddbf75 | |||
| bbf1638f20 | |||
| 010e11cf6b |
75
.claude/settings.local.json
Normal file
75
.claude/settings.local.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(./gradlew assembleDebug:*)",
|
||||||
|
"Bash(./gradlew installDebug:*)",
|
||||||
|
"Bash(adb logcat:*)",
|
||||||
|
"Bash(adb shell am start:*)",
|
||||||
|
"Bash(adb devices:*)",
|
||||||
|
"Bash(findstr:*)",
|
||||||
|
"Bash(tree:*)",
|
||||||
|
"Bash(cmake:*)",
|
||||||
|
"Bash(\".\\\\build\\\\Debug\\\\mosis-designer.exe\" \"..\\\\src\\\\main\\\\assets\\\\apps\\\\home\\\\home.rml\")",
|
||||||
|
"Bash(dir:*)",
|
||||||
|
"Bash(start \"\" \".\\\\build\\\\Debug\\\\mosis-designer.exe\" \"..\\\\src\\\\main\\\\assets\\\\apps\\\\home\\\\home.rml\")",
|
||||||
|
"Bash(\"C:\\\\Program Files\\\\AutoHotkey\\\\v2\\\\AutoHotkey64.exe\" \"D:\\\\Dev\\\\Mosis\\\\MosisService\\\\designer\\\\test\\\\diagnose.ahk\")",
|
||||||
|
"Bash(tasklist:*)",
|
||||||
|
"Bash(\"C:\\\\Program Files\\\\AutoHotkey\\\\v2\\\\AutoHotkey64.exe\" /ErrorStdOut diagnose.ahk)",
|
||||||
|
"Bash(taskkill:*)",
|
||||||
|
"Bash(\"C:\\\\Program Files\\\\AutoHotkey\\\\v2\\\\AutoHotkey64.exe\" full_click_test.ahk)",
|
||||||
|
"Bash(\"C:\\\\Program Files\\\\AutoHotkey\\\\v2\\\\AutoHotkey64.exe\" visual_click_test.ahk)",
|
||||||
|
"Bash(\".\\\\build\\\\Debug\\\\designer-test.exe\" --designer \"..\\\\designer\\\\build\\\\Debug\\\\mosis-designer.exe\" --document \"..\\\\src\\\\main\\\\assets\\\\apps\\\\home\\\\home.rml\")",
|
||||||
|
"Bash(./designer-test.exe)",
|
||||||
|
"Bash(\"D:\\\\Dev\\\\Mosis\\\\MosisService\\\\designer-test\\\\build\\\\Debug\\\\designer-test.exe\")",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(adb install:*)",
|
||||||
|
"Bash(adb shell am broadcast:*)",
|
||||||
|
"Bash(timeout 5 ./mosis-designer.exe:*)",
|
||||||
|
"Bash(.buildDebugmosis-designer.exe --dump D:DevMosisMosisServicesrcmainassetsappshomehome.rml)",
|
||||||
|
"Bash(cmd /c \"cd /d D:\\\\Dev\\\\Mosis\\\\MosisService\\\\designer && .\\\\build\\\\Debug\\\\mosis-designer.exe --dump ..\\\\src\\\\main\\\\assets\\\\apps\\\\home\\\\home.rml 2>&1\")",
|
||||||
|
"Bash(start /min cmd /c \".\\\\build\\\\Debug\\\\mosis-designer.exe ..\\\\src\\\\main\\\\assets\\\\apps\\\\home\\\\home.rml --hierarchy hierarchy.json\")",
|
||||||
|
"Bash(powershell -Command:*)",
|
||||||
|
"Bash(adb shell \"ps -A | grep mosis\")",
|
||||||
|
"Bash(adb shell:*)",
|
||||||
|
"Bash(ls:*)",
|
||||||
|
"Bash(magick:*)",
|
||||||
|
"Bash(fc:*)",
|
||||||
|
"Bash(cmp:*)",
|
||||||
|
"Bash(sort:*)",
|
||||||
|
"Bash(vcpkg search:*)",
|
||||||
|
"Bash(D:/vcpkg/vcpkg search glad)",
|
||||||
|
"Bash(timeout 8 ./mosis-designer.exe:*)",
|
||||||
|
"Bash(start mosis-designer.exe apps/home/home.rml)",
|
||||||
|
"Bash(./mosis-designer.exe:*)",
|
||||||
|
"Bash(cmd /c \"cd /d D:\\\\Dev\\\\Mosis\\\\MosisService\\\\designer && cmake --build build --config Debug\")",
|
||||||
|
"Bash(cmd /c:*)",
|
||||||
|
"Bash(\"D:/Dev/Mosis/MosisService/designer-test/build/Debug/designer-test.exe\")",
|
||||||
|
"Bash(python:*)",
|
||||||
|
"Bash(./gradlew connectedAndroidTest:*)",
|
||||||
|
"Bash(\"D:\\\\Epic\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\Build.bat\" MosisUnreal Android Development -Project=\"D:\\\\Dev\\\\Mosis\\\\MosisUnreal\\\\MosisUnreal.uproject\")",
|
||||||
|
"Bash(\"D:\\\\Epic\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\Build.bat\" MosisUnreal Win64 Development -Project=\"D:\\\\Dev\\\\Mosis\\\\MosisUnreal\\\\MosisUnreal.uproject\")",
|
||||||
|
"Bash(D:)",
|
||||||
|
"Bash(\"D:\\\\Epic\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\Build.bat\" MosisUnrealEditor Win64 Development -Project=\"D:\\\\Dev\\\\Mosis\\\\MosisUnreal\\\\MosisUnreal.uproject\")",
|
||||||
|
"Bash(\"D:\\\\Epic\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\RunUAT.bat\" BuildCookRun -project=\"D:\\\\Dev\\\\Mosis\\\\MosisUnreal\\\\MosisUnreal.uproject\" -platform=Android -clientconfig=Development -build -noP4 -utf8output)",
|
||||||
|
"Bash(\"D:\\\\Epic\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\RunUAT.bat\" BuildCookRun -project=\"D:\\\\Dev\\\\Mosis\\\\MosisUnreal\\\\MosisUnreal.uproject\" -platform=Android -clientconfig=Development -build -cook -stage -pak -package -noP4 -utf8output)",
|
||||||
|
"Bash(./gradlew.bat:*)",
|
||||||
|
"Bash(gradle assembleRelease:*)",
|
||||||
|
"Bash(\"C:\\\\Program Files\\\\Unity\\\\Hub\\\\Editor\\\\6000.3.2f1\\\\Editor\\\\Unity.exe\" -batchmode -quit -nographics -projectPath \"D:\\\\Dev\\\\Mosis\\\\MosisVR\" -executeMethod BuildScript.BuildAndroidCI -outputPath \"D:\\\\Dev\\\\Mosis\\\\Builds\\\\Unity\\\\Android\\\\MosisVR.apk\" -logFile \"D:\\\\Dev\\\\Mosis\\\\Builds\\\\Unity\\\\build3.log\")",
|
||||||
|
"Bash(gradlew.bat assembleRelease)",
|
||||||
|
"Bash(\"C:\\\\Program Files\\\\Unity\\\\Hub\\\\Editor\\\\6000.3.2f1\\\\Editor\\\\Unity.exe\" -batchmode -quit -nographics -projectPath \"D:\\\\Dev\\\\Mosis\\\\MosisVR\" -executeMethod BuildScript.BuildAndroidDirectCI -outputPath \"D:\\\\Dev\\\\Mosis\\\\Builds\\\\Unity\\\\Android\\\\MosisVR-direct.apk\" -logFile \"D:\\\\Dev\\\\Mosis\\\\Builds\\\\Unity\\\\build-direct.log\")",
|
||||||
|
"Bash(gradlew assembleDebug:*)",
|
||||||
|
"Bash(\"D:\\\\Epic\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\RunUAT.bat\" BuildCookRun -project=\"D:\\\\Dev\\\\Mosis\\\\MosisUnreal\\\\MosisUnreal.uproject\" -platform=Android -clientconfig=Development -build -cook -stage -pak -package -archive -archivedirectory=\"D:\\\\Dev\\\\Mosis\\\\Builds\\\\Unreal\" -noP4)",
|
||||||
|
"Bash(adb:*)",
|
||||||
|
"Bash(\"D:/Epic/UE_5.5/Engine/Build/BatchFiles/RunUAT.bat\" BuildCookRun -project=\"D:/Dev/Mosis/MosisUnreal/MosisUnreal.uproject\" -platform=Android -clientconfig=Development -build -cook -stage -pak -package -noP4)",
|
||||||
|
"Bash(MSYS_NO_PATHCONV=1 adb:*)",
|
||||||
|
"Bash(\"D:/Epic/UE_5.5/Engine/Build/BatchFiles/Build.bat\" MosisUnrealEditor Win64 Development -Project=\"D:/Dev/Mosis/MosisUnreal/MosisUnreal.uproject\")",
|
||||||
|
"Bash(\"D:/Epic/UE_5.5/Engine/Build/BatchFiles/Build.bat\" MosisUnreal Android Development -Project=\"D:/Dev/Mosis/MosisUnreal/MosisUnreal.uproject\")",
|
||||||
|
"Bash(\"D:/Epic/UE_5.5/Engine/Build/BatchFiles/RunUAT.bat\" BuildCookRun -project=\"D:/Dev/Mosis/MosisUnreal/MosisUnreal.uproject\" -platform=Android -clientconfig=Development -build -cook -stage -pak -package -noP4 -utf8output)",
|
||||||
|
"Bash(git -C \"D:/Dev/Mosis/MosisUnreal\" add Plugins/MosisSDK/MosisSDK.uplugin Plugins/MosisSDK/Source/MosisSDK/MosisSDK.Build.cs Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneActor.cpp Plugins/MosisSDK/Source/MosisSDK/Private/MosisPointerComponent.cpp Plugins/MosisSDK/Source/MosisSDK/Public/MosisPointerComponent.h)"
|
||||||
|
],
|
||||||
|
"additionalDirectories": [
|
||||||
|
"D:\\Dev\\Mosis\\MosisUnreal",
|
||||||
|
"D:\\Dev\\Mosis\\MosisVR"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
# MosisService Architecture
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
MosisService is an Android application that combines Kotlin UI components with native C++ libraries for UI rendering and system interaction. The architecture is built around a service-oriented design using Android's Binder system and integrates with RmlUi for rich UI rendering.
|
|
||||||
|
|
||||||
## Core Components
|
|
||||||
|
|
||||||
### 1. Service Layer (mosis-service)
|
|
||||||
- **Purpose**: Main Android Binder service implementation
|
|
||||||
- **Interface**: Implements `IMosisService.aidl`
|
|
||||||
- **Functionality**:
|
|
||||||
- Touch event processing (onTouchDown, onTouchMove, onTouchUp)
|
|
||||||
- Service initialization and listener management
|
|
||||||
- Integration with kernel-based rendering system
|
|
||||||
- Asset loading through Android AssetManager
|
|
||||||
|
|
||||||
### 2. Rendering Layer (mosis-test)
|
|
||||||
- **Purpose**: UI rendering and testing infrastructure
|
|
||||||
- **Interface**: Implements `IMosisListener.aidl`
|
|
||||||
- **Functionality**:
|
|
||||||
- OpenGL ES 2.0 rendering pipeline using GLAD
|
|
||||||
- Multi-threaded rendering with EGL context management
|
|
||||||
- Surface and buffer handling for Android native windows
|
|
||||||
- Task queue management for asynchronous rendering operations
|
|
||||||
- Hardware buffer management and processing
|
|
||||||
|
|
||||||
### 3. Core Engine (Kernel)
|
|
||||||
- **Purpose**: Central rendering and event processing engine
|
|
||||||
- **Components**:
|
|
||||||
- RmlUi-based UI rendering engine
|
|
||||||
- EGL context creation and management
|
|
||||||
- Render target handling
|
|
||||||
- Touch event processing and UI updates
|
|
||||||
- Multi-threaded execution using std::thread
|
|
||||||
|
|
||||||
### 4. Supporting Libraries
|
|
||||||
- **AssetsManager**: Asset loading from Android AssetManager
|
|
||||||
- **Logger**: Cross-platform logging system
|
|
||||||
- **EGL Context**: OpenGL ES context management
|
|
||||||
- **Render Target**: Framebuffer and buffer management
|
|
||||||
- **Shader**: OpenGL shader program handling
|
|
||||||
- **External Texture**: Hardware buffer texture creation
|
|
||||||
|
|
||||||
## System Architecture
|
|
||||||
|
|
||||||
### Android Binder Integration
|
|
||||||
- Uses AIDL (Android Interface Definition Language) for cross-process communication
|
|
||||||
- Service exposes `IMosisService` interface for touch events and initialization
|
|
||||||
- Listener pattern with `IMosisListener` for rendering callbacks
|
|
||||||
- Binds to Java/Kotlin components through JNI
|
|
||||||
|
|
||||||
### Multi-threading Model
|
|
||||||
- Service layer runs in main thread for event handling
|
|
||||||
- Rendering loop runs in dedicated thread managed by Kernel
|
|
||||||
- Async task processing for UI updates
|
|
||||||
- Thread-safe communication between components
|
|
||||||
|
|
||||||
### Rendering Pipeline
|
|
||||||
1. **Initialization**: Service connects to test layer, creates EGL context
|
|
||||||
2. **Buffer Management**: Hardware buffers allocated and shared between layers
|
|
||||||
3. **Event Processing**: Touch events processed and forwarded to Kernel
|
|
||||||
4. **Rendering Loop**: Continuous rendering with frame synchronization
|
|
||||||
5. **UI Updates**: RmlUi engine updates UI based on events and data
|
|
||||||
|
|
||||||
### Data Flow
|
|
||||||
```
|
|
||||||
User Touch Events → IMosisService.onTouch* → Kernel.process → RmlUi UI Updates
|
|
||||||
Service Initialized → IMosisListener.onServiceInitialized → Rendering Setup
|
|
||||||
Buffer Available → IMosisListener.onBufferAvailable → Texture Creation
|
|
||||||
Frame Available → IMosisListener.onFrameAvailable → Frame Rendering
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Technologies
|
|
||||||
- **Android NDK**: Native development with C++23
|
|
||||||
- **CMake**: Build system for native libraries
|
|
||||||
- **RmlUi**: UI rendering engine with HTML/CSS-like markup
|
|
||||||
- **OpenGL ES 2.0**: Graphics rendering
|
|
||||||
- **GLAD**: OpenGL loader library
|
|
||||||
- **AIDL**: Inter-process communication
|
|
||||||
- **Binder**: Android's IPC mechanism
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
- `mosis-service.cpp`: Main service implementation
|
|
||||||
- `mosis-test.cpp`: Test/rendering implementation
|
|
||||||
- `kernel.cpp`: Core rendering engine and event processing
|
|
||||||
- `assets_manager.cpp`: Asset loading utilities
|
|
||||||
- `egl_context.cpp`: EGL context management
|
|
||||||
- `render_target.cpp`: Framebuffer handling
|
|
||||||
- `logger.cpp`: Cross-platform logging
|
|
||||||
|
|
||||||
## Code Standards
|
|
||||||
- Modern C++23 with smart pointers and RAII
|
|
||||||
- Thread-safe operations using std::mutex
|
|
||||||
- Resource management with proper cleanup
|
|
||||||
- Use of std::span, std::format for modern features
|
|
||||||
- Cross-platform compatibility through Android NDK
|
|
||||||
3569
SANDBOX.md
3569
SANDBOX.md
File diff suppressed because it is too large
Load Diff
@@ -55,6 +55,14 @@ target_compile_definitions(mosis-kernel PUBLIC
|
|||||||
RMLUI_GL3_CUSTOM_LOADER="glad_loader.h"
|
RMLUI_GL3_CUSTOM_LOADER="glad_loader.h"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Sandbox sources (reuse from MosisService)
|
||||||
|
set(SANDBOX_SOURCES
|
||||||
|
../src/main/cpp/sandbox/timer_manager.cpp
|
||||||
|
../src/main/cpp/sandbox/json_api.cpp
|
||||||
|
../src/main/cpp/sandbox/crypto_api.cpp
|
||||||
|
../src/main/cpp/sandbox/virtual_fs.cpp
|
||||||
|
)
|
||||||
|
|
||||||
# Designer executable
|
# Designer executable
|
||||||
add_executable(mosis-designer
|
add_executable(mosis-designer
|
||||||
main.cpp
|
main.cpp
|
||||||
@@ -62,6 +70,7 @@ add_executable(mosis-designer
|
|||||||
src/desktop_file_interface.cpp
|
src/desktop_file_interface.cpp
|
||||||
src/hot_reload.cpp
|
src/hot_reload.cpp
|
||||||
src/platform_singleton.cpp
|
src/platform_singleton.cpp
|
||||||
|
src/desktop_sandbox.cpp
|
||||||
src/testing/action_recorder.cpp
|
src/testing/action_recorder.cpp
|
||||||
src/testing/action_player.cpp
|
src/testing/action_player.cpp
|
||||||
src/testing/ui_inspector.cpp
|
src/testing/ui_inspector.cpp
|
||||||
@@ -69,6 +78,8 @@ add_executable(mosis-designer
|
|||||||
# Local backend with input recording hooks
|
# Local backend with input recording hooks
|
||||||
src/backend/RmlUi_Backend_GLFW_GL3.cpp
|
src/backend/RmlUi_Backend_GLFW_GL3.cpp
|
||||||
src/backend/RmlUi_Platform_GLFW.cpp
|
src/backend/RmlUi_Platform_GLFW.cpp
|
||||||
|
# Sandbox APIs
|
||||||
|
${SANDBOX_SOURCES}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(mosis-designer PRIVATE
|
target_include_directories(mosis-designer PRIVATE
|
||||||
@@ -77,6 +88,7 @@ target_include_directories(mosis-designer PRIVATE
|
|||||||
src/backend
|
src/backend
|
||||||
../src/main/kernel/include
|
../src/main/kernel/include
|
||||||
../src/main/cpp
|
../src/main/cpp
|
||||||
|
../src/main/cpp/sandbox
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(mosis-designer PRIVATE
|
target_link_libraries(mosis-designer PRIVATE
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include "src/desktop_sandbox.h"
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ static mosis::HotReload* g_hot_reload = nullptr;
|
|||||||
static std::string g_current_document_path;
|
static std::string g_current_document_path;
|
||||||
static std::string g_current_screen_url; // For hierarchy dump - tracks current screen
|
static std::string g_current_screen_url; // For hierarchy dump - tracks current screen
|
||||||
static bool g_needs_reload = false;
|
static bool g_needs_reload = false;
|
||||||
|
static std::unique_ptr<mosis::DesktopSandbox> g_sandbox;
|
||||||
|
|
||||||
// Resolution presets
|
// Resolution presets
|
||||||
static int g_width = 540;
|
static int g_width = 540;
|
||||||
@@ -126,36 +128,28 @@ static void MouseButtonCallback(GLFWwindow* window, int button, int action, int
|
|||||||
double xpos, ypos;
|
double xpos, ypos;
|
||||||
glfwGetCursorPos(window, &xpos, &ypos);
|
glfwGetCursorPos(window, &xpos, &ypos);
|
||||||
|
|
||||||
// Convert from physical (GLFW) to logical (RmlUi) coordinates
|
// GLFW cursor callbacks report coordinates in screen/window coordinates (logical pixels)
|
||||||
// GLFW reports physical pixels, but RmlUi context uses logical pixels
|
// which match the RmlUi context dimensions, so no scaling needed
|
||||||
float scaleX, scaleY;
|
int mouseX = static_cast<int>(xpos);
|
||||||
glfwGetWindowContentScale(window, &scaleX, &scaleY);
|
int mouseY = static_cast<int>(ypos);
|
||||||
int logicalX = static_cast<int>(xpos / scaleX);
|
|
||||||
int logicalY = static_cast<int>(ypos / scaleY);
|
|
||||||
|
|
||||||
// Debug log for click detection
|
|
||||||
Rml::Log::Message(Rml::Log::LT_INFO, "Mouse %s at physical(%.0f, %.0f) logical(%d, %d) button=%d",
|
|
||||||
action == GLFW_PRESS ? "down" : "up", xpos, ypos, logicalX, logicalY, button);
|
|
||||||
|
|
||||||
int key_modifier = 0;
|
int key_modifier = 0;
|
||||||
if (mods & GLFW_MOD_CONTROL) key_modifier |= Rml::Input::KM_CTRL;
|
if (mods & GLFW_MOD_CONTROL) key_modifier |= Rml::Input::KM_CTRL;
|
||||||
if (mods & GLFW_MOD_SHIFT) key_modifier |= Rml::Input::KM_SHIFT;
|
if (mods & GLFW_MOD_SHIFT) key_modifier |= Rml::Input::KM_SHIFT;
|
||||||
if (mods & GLFW_MOD_ALT) key_modifier |= Rml::Input::KM_ALT;
|
if (mods & GLFW_MOD_ALT) key_modifier |= Rml::Input::KM_ALT;
|
||||||
|
|
||||||
// Update mouse position before processing button event (using logical coords)
|
// Update mouse position before processing button event
|
||||||
g_context->ProcessMouseMove(logicalX, logicalY, key_modifier);
|
g_context->ProcessMouseMove(mouseX, mouseY, key_modifier);
|
||||||
|
|
||||||
if (button == GLFW_MOUSE_BUTTON_LEFT) {
|
if (button == GLFW_MOUSE_BUTTON_LEFT) {
|
||||||
if (action == GLFW_PRESS) {
|
if (action == GLFW_PRESS) {
|
||||||
// Record mouse down in record mode (use logical coords)
|
|
||||||
if (g_action_recorder && g_action_recorder->IsRecording()) {
|
if (g_action_recorder && g_action_recorder->IsRecording()) {
|
||||||
g_action_recorder->RecordMouseDown(logicalX, logicalY);
|
g_action_recorder->RecordMouseDown(mouseX, mouseY);
|
||||||
}
|
}
|
||||||
g_context->ProcessMouseButtonDown(0, key_modifier);
|
g_context->ProcessMouseButtonDown(0, key_modifier);
|
||||||
} else if (action == GLFW_RELEASE) {
|
} else if (action == GLFW_RELEASE) {
|
||||||
// Record mouse up in record mode
|
|
||||||
if (g_action_recorder && g_action_recorder->IsRecording()) {
|
if (g_action_recorder && g_action_recorder->IsRecording()) {
|
||||||
g_action_recorder->RecordMouseUp(logicalX, logicalY);
|
g_action_recorder->RecordMouseUp(mouseX, mouseY);
|
||||||
}
|
}
|
||||||
g_context->ProcessMouseButtonUp(0, key_modifier);
|
g_context->ProcessMouseButtonUp(0, key_modifier);
|
||||||
}
|
}
|
||||||
@@ -165,13 +159,12 @@ static void MouseButtonCallback(GLFWwindow* window, int button, int action, int
|
|||||||
static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos) {
|
static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos) {
|
||||||
if (!g_context) return;
|
if (!g_context) return;
|
||||||
|
|
||||||
// Convert from physical to logical coordinates
|
// GLFW cursor callbacks report coordinates in screen/window coordinates (logical pixels)
|
||||||
float scaleX, scaleY;
|
// which match the RmlUi context dimensions, so no scaling needed
|
||||||
glfwGetWindowContentScale(window, &scaleX, &scaleY);
|
int mouseX = static_cast<int>(xpos);
|
||||||
int logicalX = static_cast<int>(xpos / scaleX);
|
int mouseY = static_cast<int>(ypos);
|
||||||
int logicalY = static_cast<int>(ypos / scaleY);
|
|
||||||
|
|
||||||
g_context->ProcessMouseMove(logicalX, logicalY, 0);
|
g_context->ProcessMouseMove(mouseX, mouseY, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
|
static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
|
||||||
@@ -291,10 +284,11 @@ int main(int argc, char* argv[]) {
|
|||||||
if (!assets_path_specified) {
|
if (!assets_path_specified) {
|
||||||
fs::path doc_path = fs::absolute(document_path);
|
fs::path doc_path = fs::absolute(document_path);
|
||||||
fs::path current = doc_path.parent_path();
|
fs::path current = doc_path.parent_path();
|
||||||
|
fs::path prev_path;
|
||||||
|
|
||||||
// Walk up the directory tree looking for a folder that ends with "assets"
|
// Walk up the directory tree looking for a folder that ends with "assets"
|
||||||
// or contains typical asset folders like "apps", "ui", "fonts"
|
// or contains typical asset folders like "apps", "ui", "fonts"
|
||||||
while (!current.empty() && current.has_parent_path()) {
|
while (!current.empty() && current != prev_path) {
|
||||||
std::string folder_name = current.filename().string();
|
std::string folder_name = current.filename().string();
|
||||||
if (folder_name == "assets") {
|
if (folder_name == "assets") {
|
||||||
assets_path = current.string();
|
assets_path = current.string();
|
||||||
@@ -305,12 +299,38 @@ int main(int argc, char* argv[]) {
|
|||||||
assets_path = current.string();
|
assets_path = current.string();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
prev_path = current;
|
||||||
current = current.parent_path();
|
current = current.parent_path();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to "assets" relative to executable if not found
|
// Fall back options if no standard assets folder found
|
||||||
if (assets_path.empty()) {
|
if (assets_path.empty()) {
|
||||||
assets_path = "assets";
|
// If the document exists, use its parent directory as assets path
|
||||||
|
fs::path doc_path = fs::absolute(document_path);
|
||||||
|
if (fs::exists(doc_path)) {
|
||||||
|
assets_path = doc_path.parent_path().string();
|
||||||
|
} else {
|
||||||
|
// Try executable's assets folder
|
||||||
|
fs::path exe_path = fs::path(argv[0]).parent_path();
|
||||||
|
if (exe_path.empty()) {
|
||||||
|
exe_path = ".";
|
||||||
|
}
|
||||||
|
fs::path exe_assets = fs::absolute(exe_path) / "assets";
|
||||||
|
if (fs::exists(exe_assets)) {
|
||||||
|
assets_path = exe_assets.string();
|
||||||
|
} else {
|
||||||
|
// Last resort: current directory
|
||||||
|
assets_path = fs::absolute(".").string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make document_path absolute if it's relative and exists
|
||||||
|
if (!fs::path(document_path).is_absolute()) {
|
||||||
|
fs::path abs_doc = fs::absolute(document_path);
|
||||||
|
if (fs::exists(abs_doc)) {
|
||||||
|
document_path = abs_doc.string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,8 +472,7 @@ int main(int argc, char* argv[]) {
|
|||||||
g_render_interface->EndFrame(0);
|
g_render_interface->EndFrame(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
glfwSwapBuffers(g_window);
|
// Capture screenshot BEFORE swap (glReadPixels reads from back buffer)
|
||||||
|
|
||||||
if (g_test_mode == TestMode::Screenshot) {
|
if (g_test_mode == TestMode::Screenshot) {
|
||||||
mosis::testing::VisualCapture capture(fb_width, fb_height);
|
mosis::testing::VisualCapture capture(fb_width, fb_height);
|
||||||
if (capture.CaptureScreenshot(g_test_output_path)) {
|
if (capture.CaptureScreenshot(g_test_output_path)) {
|
||||||
@@ -470,6 +489,8 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
glfwSwapBuffers(g_window);
|
||||||
|
|
||||||
// Cleanup and exit
|
// Cleanup and exit
|
||||||
ShutdownRmlUi();
|
ShutdownRmlUi();
|
||||||
glfwDestroyWindow(g_window);
|
glfwDestroyWindow(g_window);
|
||||||
@@ -503,6 +524,11 @@ int main(int argc, char* argv[]) {
|
|||||||
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
|
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
// Update sandbox (process timers)
|
||||||
|
if (g_sandbox) {
|
||||||
|
g_sandbox->Update();
|
||||||
|
}
|
||||||
|
|
||||||
// Update and render
|
// Update and render
|
||||||
if (g_context) {
|
if (g_context) {
|
||||||
g_context->Update();
|
g_context->Update();
|
||||||
@@ -541,6 +567,7 @@ int main(int argc, char* argv[]) {
|
|||||||
if (g_log_file.is_open()) {
|
if (g_log_file.is_open()) {
|
||||||
g_log_file.close();
|
g_log_file.close();
|
||||||
}
|
}
|
||||||
|
g_sandbox.reset();
|
||||||
ShutdownRmlUi();
|
ShutdownRmlUi();
|
||||||
glfwDestroyWindow(g_window);
|
glfwDestroyWindow(g_window);
|
||||||
glfwTerminate();
|
glfwTerminate();
|
||||||
@@ -572,8 +599,16 @@ bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height
|
|||||||
// Initialize Lua bindings
|
// Initialize Lua bindings
|
||||||
Rml::Lua::Initialise();
|
Rml::Lua::Initialise();
|
||||||
|
|
||||||
// Register loadScreen function for navigation
|
// Get Lua state
|
||||||
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
|
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
|
||||||
|
|
||||||
|
// Initialize sandbox with APIs (timers, JSON, crypto, fs)
|
||||||
|
mosis::DesktopSandboxConfig sandbox_config;
|
||||||
|
sandbox_config.data_root = assets_path + "/sandbox_data";
|
||||||
|
g_sandbox = std::make_unique<mosis::DesktopSandbox>(sandbox_config);
|
||||||
|
g_sandbox->RegisterAPIs(L);
|
||||||
|
|
||||||
|
// Register loadScreen function for navigation
|
||||||
lua_pushcfunction(L, [](lua_State* L) -> int {
|
lua_pushcfunction(L, [](lua_State* L) -> int {
|
||||||
const char* path = luaL_checkstring(L, 1);
|
const char* path = luaL_checkstring(L, 1);
|
||||||
|
|
||||||
@@ -610,15 +645,33 @@ bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height
|
|||||||
lua_setglobal(L, "loadScreen");
|
lua_setglobal(L, "loadScreen");
|
||||||
std::cout << "Registered Lua loadScreen function" << std::endl;
|
std::cout << "Registered Lua loadScreen function" << std::endl;
|
||||||
|
|
||||||
// Load fonts
|
// Load fonts - search for fonts directory in multiple locations
|
||||||
std::vector<std::string> fonts = {
|
std::string fonts_root;
|
||||||
"fonts/LatoLatin-Regular.ttf",
|
std::vector<std::string> font_search_paths = {
|
||||||
"fonts/LatoLatin-Bold.ttf",
|
assets_path + "/fonts",
|
||||||
"fonts/LatoLatin-Light.ttf",
|
assets_path + "/../src/main/assets/fonts", // If assets_path is test-app dir
|
||||||
|
assets_path + "/../../src/main/assets/fonts",
|
||||||
|
std::filesystem::absolute("src/main/assets/fonts").string(),
|
||||||
};
|
};
|
||||||
for (const auto& font : fonts) {
|
for (const auto& search_path : font_search_paths) {
|
||||||
if (!Rml::LoadFontFace(font)) {
|
if (std::filesystem::exists(search_path + "/LatoLatin-Regular.ttf")) {
|
||||||
std::cerr << "Warning: Failed to load font: " << font << std::endl;
|
fonts_root = search_path;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fonts_root.empty()) {
|
||||||
|
std::cerr << "Warning: Could not find fonts directory" << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << "Fonts path: " << fonts_root << std::endl;
|
||||||
|
std::vector<std::string> fonts = {
|
||||||
|
fonts_root + "/LatoLatin-Regular.ttf",
|
||||||
|
fonts_root + "/LatoLatin-Bold.ttf",
|
||||||
|
fonts_root + "/LatoLatin-Italic.ttf",
|
||||||
|
};
|
||||||
|
for (const auto& font : fonts) {
|
||||||
|
if (!Rml::LoadFontFace(font)) {
|
||||||
|
std::cerr << "Warning: Failed to load font: " << font << std::endl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,7 +729,15 @@ bool LoadDocument(const std::string& path) {
|
|||||||
|
|
||||||
void ReloadDocument() {
|
void ReloadDocument() {
|
||||||
std::cout << "Reloading..." << std::endl;
|
std::cout << "Reloading..." << std::endl;
|
||||||
|
|
||||||
|
// Reset sandbox (clear timers, re-register APIs)
|
||||||
|
if (g_sandbox) {
|
||||||
|
lua_State* L = Rml::Lua::Interpreter::GetLuaState();
|
||||||
|
g_sandbox->UnregisterAPIs(L);
|
||||||
|
g_sandbox->Reset();
|
||||||
|
g_sandbox->RegisterAPIs(L);
|
||||||
|
}
|
||||||
|
|
||||||
// Reload stylesheets
|
// Reload stylesheets
|
||||||
for (int i = 0; i < g_context->GetNumDocuments(); ++i) {
|
for (int i = 0; i < g_context->GetNumDocuments(); ++i) {
|
||||||
auto* doc = g_context->GetDocument(i);
|
auto* doc = g_context->GetDocument(i);
|
||||||
|
|||||||
167
designer/src/desktop_sandbox.cpp
Normal file
167
designer/src/desktop_sandbox.cpp
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
#include "desktop_sandbox.h"
|
||||||
|
#include "timer_manager.h"
|
||||||
|
#include "json_api.h"
|
||||||
|
#include "crypto_api.h"
|
||||||
|
#include "virtual_fs.h"
|
||||||
|
#include <lua.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
DesktopSandbox::DesktopSandbox(const DesktopSandboxConfig& config)
|
||||||
|
: m_config(config) {
|
||||||
|
// Create timer manager
|
||||||
|
m_timer_manager = std::make_unique<TimerManager>();
|
||||||
|
|
||||||
|
// Create virtual filesystem
|
||||||
|
// Ensure data root exists
|
||||||
|
std::error_code ec;
|
||||||
|
fs::create_directories(m_config.data_root, ec);
|
||||||
|
|
||||||
|
m_vfs = std::make_unique<VirtualFS>(
|
||||||
|
m_config.app_id,
|
||||||
|
m_config.data_root,
|
||||||
|
VirtualFSLimits{}
|
||||||
|
);
|
||||||
|
|
||||||
|
std::cout << "DesktopSandbox initialized for app: " << m_config.app_id << std::endl;
|
||||||
|
std::cout << " Data root: " << m_config.data_root << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopSandbox::~DesktopSandbox() {
|
||||||
|
std::cout << "DesktopSandbox destroyed" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// crypto.sha256(data) -> hash
|
||||||
|
// Convenience wrapper for crypto.hash("sha256", data)
|
||||||
|
static int lua_crypto_sha256(lua_State* L) {
|
||||||
|
// Get the crypto table
|
||||||
|
lua_getglobal(L, "crypto");
|
||||||
|
if (!lua_istable(L, -1)) {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return luaL_error(L, "crypto table not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get crypto.hash function
|
||||||
|
lua_getfield(L, -1, "hash");
|
||||||
|
if (!lua_isfunction(L, -1)) {
|
||||||
|
lua_pop(L, 2);
|
||||||
|
return luaL_error(L, "crypto.hash function not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call hash("sha256", data)
|
||||||
|
lua_pushstring(L, "sha256");
|
||||||
|
lua_pushvalue(L, 1); // Push the original data argument
|
||||||
|
lua_call(L, 2, 1);
|
||||||
|
|
||||||
|
// Remove the crypto table from stack, keep result
|
||||||
|
lua_remove(L, -2);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// crypto.sha512(data) -> hash
|
||||||
|
static int lua_crypto_sha512(lua_State* L) {
|
||||||
|
lua_getglobal(L, "crypto");
|
||||||
|
if (!lua_istable(L, -1)) {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return luaL_error(L, "crypto table not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_getfield(L, -1, "hash");
|
||||||
|
if (!lua_isfunction(L, -1)) {
|
||||||
|
lua_pop(L, 2);
|
||||||
|
return luaL_error(L, "crypto.hash function not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_pushstring(L, "sha512");
|
||||||
|
lua_pushvalue(L, 1);
|
||||||
|
lua_call(L, 2, 1);
|
||||||
|
|
||||||
|
lua_remove(L, -2);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopSandbox::RegisterAPIs(lua_State* L) {
|
||||||
|
std::cout << "Registering sandbox APIs..." << std::endl;
|
||||||
|
|
||||||
|
// Register timer API (setTimeout, setInterval, clearTimeout, clearInterval)
|
||||||
|
RegisterTimerAPI(L, m_timer_manager.get(), m_config.app_id);
|
||||||
|
std::cout << " Timer API registered" << std::endl;
|
||||||
|
|
||||||
|
// Register JSON API (json.encode, json.decode)
|
||||||
|
RegisterJsonAPI(L, JsonLimits{});
|
||||||
|
std::cout << " JSON API registered" << std::endl;
|
||||||
|
|
||||||
|
// Register crypto API (crypto.randomBytes, crypto.hash, crypto.hmac)
|
||||||
|
RegisterCryptoAPI(L);
|
||||||
|
|
||||||
|
// Add convenience wrappers for common hash algorithms
|
||||||
|
lua_getglobal(L, "crypto");
|
||||||
|
if (lua_istable(L, -1)) {
|
||||||
|
lua_pushcfunction(L, lua_crypto_sha256);
|
||||||
|
lua_setfield(L, -2, "sha256");
|
||||||
|
|
||||||
|
lua_pushcfunction(L, lua_crypto_sha512);
|
||||||
|
lua_setfield(L, -2, "sha512");
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
std::cout << " Crypto API registered" << std::endl;
|
||||||
|
|
||||||
|
// Register virtual filesystem API (fs.read, fs.write, fs.delete, etc.)
|
||||||
|
RegisterVirtualFS(L, m_vfs.get());
|
||||||
|
std::cout << " VirtualFS API registered" << std::endl;
|
||||||
|
|
||||||
|
std::cout << "All sandbox APIs registered" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopSandbox::UnregisterAPIs(lua_State* L) {
|
||||||
|
// Clear the global tables by setting them to nil
|
||||||
|
// This ensures clean state on hot-reload
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_setglobal(L, "setTimeout");
|
||||||
|
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_setglobal(L, "clearTimeout");
|
||||||
|
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_setglobal(L, "setInterval");
|
||||||
|
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_setglobal(L, "clearInterval");
|
||||||
|
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_setglobal(L, "json");
|
||||||
|
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_setglobal(L, "crypto");
|
||||||
|
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_setglobal(L, "fs");
|
||||||
|
|
||||||
|
std::cout << "Sandbox APIs unregistered" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopSandbox::Update() {
|
||||||
|
// Process any pending timers
|
||||||
|
if (m_timer_manager) {
|
||||||
|
m_timer_manager->ProcessTimers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopSandbox::Reset() {
|
||||||
|
// Clear all timers for the app
|
||||||
|
if (m_timer_manager) {
|
||||||
|
m_timer_manager->ClearAppTimers(m_config.app_id);
|
||||||
|
std::cout << "Timers cleared for hot-reload" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally clear temp files
|
||||||
|
if (m_vfs) {
|
||||||
|
m_vfs->ClearTemp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
33
designer/src/desktop_sandbox.h
Normal file
33
designer/src/desktop_sandbox.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
struct lua_State;
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
class TimerManager;
|
||||||
|
class VirtualFS;
|
||||||
|
|
||||||
|
struct DesktopSandboxConfig {
|
||||||
|
std::string app_id = "com.mosis.designer";
|
||||||
|
std::string data_root = "./sandbox_data";
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopSandbox {
|
||||||
|
public:
|
||||||
|
explicit DesktopSandbox(const DesktopSandboxConfig& config);
|
||||||
|
~DesktopSandbox();
|
||||||
|
|
||||||
|
void RegisterAPIs(lua_State* L);
|
||||||
|
void UnregisterAPIs(lua_State* L);
|
||||||
|
void Update(); // Process timers - call each frame
|
||||||
|
void Reset(); // For hot-reload
|
||||||
|
|
||||||
|
private:
|
||||||
|
DesktopSandboxConfig m_config;
|
||||||
|
std::unique_ptr<TimerManager> m_timer_manager;
|
||||||
|
std::unique_ptr<VirtualFS> m_vfs;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
@@ -22,14 +22,24 @@ void HotReload::Start() {
|
|||||||
|
|
||||||
m_running = true;
|
m_running = true;
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
// Create a manual-reset event for signaling shutdown
|
||||||
|
m_stop_event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||||
|
if (!m_stop_event) {
|
||||||
|
std::cerr << "Failed to create stop event" << std::endl;
|
||||||
|
m_running = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_notification_handle = FindFirstChangeNotificationW(
|
m_notification_handle = FindFirstChangeNotificationW(
|
||||||
m_watch_path.c_str(),
|
m_watch_path.c_str(),
|
||||||
TRUE, // Watch subtree
|
TRUE, // Watch subtree
|
||||||
FILE_NOTIFY_CHANGE_LAST_WRITE
|
FILE_NOTIFY_CHANGE_LAST_WRITE
|
||||||
);
|
);
|
||||||
|
|
||||||
if (m_notification_handle == INVALID_HANDLE_VALUE) {
|
if (m_notification_handle == INVALID_HANDLE_VALUE) {
|
||||||
std::cerr << "Failed to set up file watching" << std::endl;
|
std::cerr << "Failed to set up file watching" << std::endl;
|
||||||
|
CloseHandle(m_stop_event);
|
||||||
|
m_stop_event = nullptr;
|
||||||
m_running = false;
|
m_running = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -39,6 +49,12 @@ void HotReload::Start() {
|
|||||||
|
|
||||||
void HotReload::Stop() {
|
void HotReload::Stop() {
|
||||||
m_running = false;
|
m_running = false;
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Signal the stop event to wake up the watch thread
|
||||||
|
if (m_stop_event) {
|
||||||
|
SetEvent(m_stop_event);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (m_watch_thread.joinable()) {
|
if (m_watch_thread.joinable()) {
|
||||||
m_watch_thread.join();
|
m_watch_thread.join();
|
||||||
}
|
}
|
||||||
@@ -47,21 +63,33 @@ void HotReload::Stop() {
|
|||||||
FindCloseChangeNotification(m_notification_handle);
|
FindCloseChangeNotification(m_notification_handle);
|
||||||
m_notification_handle = nullptr;
|
m_notification_handle = nullptr;
|
||||||
}
|
}
|
||||||
|
if (m_stop_event) {
|
||||||
|
CloseHandle(m_stop_event);
|
||||||
|
m_stop_event = nullptr;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void HotReload::WatchThread() {
|
void HotReload::WatchThread() {
|
||||||
while (m_running) {
|
while (m_running) {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
DWORD result = WaitForSingleObject(m_notification_handle, 100); // 100ms timeout
|
// Wait on both the file change notification and the stop event
|
||||||
|
HANDLE handles[2] = { m_notification_handle, m_stop_event };
|
||||||
|
DWORD result = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
|
||||||
|
|
||||||
if (result == WAIT_OBJECT_0) {
|
if (result == WAIT_OBJECT_0) {
|
||||||
|
// File change notification
|
||||||
// Debounce - wait a bit for file writes to complete
|
// Debounce - wait a bit for file writes to complete
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
if (m_callback) {
|
if (m_callback && m_running) {
|
||||||
m_callback();
|
m_callback();
|
||||||
}
|
}
|
||||||
FindNextChangeNotification(m_notification_handle);
|
FindNextChangeNotification(m_notification_handle);
|
||||||
|
} else if (result == WAIT_OBJECT_0 + 1) {
|
||||||
|
// Stop event signaled - exit the loop
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
// WAIT_FAILED or other errors will just loop and check m_running
|
||||||
#else
|
#else
|
||||||
// TODO: Linux inotify / macOS FSEvents implementation
|
// TODO: Linux inotify / macOS FSEvents implementation
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ private:
|
|||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
void* m_notification_handle = nullptr; // HANDLE
|
void* m_notification_handle = nullptr; // HANDLE
|
||||||
|
void* m_stop_event = nullptr; // HANDLE for signaling shutdown
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
129
docs/ANDROID-TESTING.md
Normal file
129
docs/ANDROID-TESTING.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Android Device Testing
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check connected device
|
||||||
|
adb devices -l
|
||||||
|
|
||||||
|
# Verify Mosis app is installed
|
||||||
|
adb shell pm list packages | grep mosis
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build and Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build debug APK
|
||||||
|
./gradlew assembleDebug
|
||||||
|
|
||||||
|
# Install on device
|
||||||
|
adb install -r build/outputs/apk/debug/MosisService-debug.apk
|
||||||
|
|
||||||
|
# Launch the app
|
||||||
|
adb shell am start -n com.omixlab.mosis/.MainActivity
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run Gradle Connected Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all connected Android tests
|
||||||
|
./gradlew connectedAndroidTest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Injection via ADB
|
||||||
|
|
||||||
|
Inject touch events for automated testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Click at normalized coordinates (0.0-1.0)
|
||||||
|
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
|
||||||
|
--es touch_type "click" --ef x 0.5 --ef y 0.5
|
||||||
|
|
||||||
|
# Touch down
|
||||||
|
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
|
||||||
|
--es touch_type "down" --ef x 0.2 --ef y 0.9
|
||||||
|
|
||||||
|
# Touch up
|
||||||
|
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
|
||||||
|
--es touch_type "up" --ef x 0.2 --ef y 0.9
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dock Element Coordinates (Normalized)
|
||||||
|
|
||||||
|
| Element | X | Y |
|
||||||
|
|---------|---|---|
|
||||||
|
| dock-phone | 0.16 | 0.97 |
|
||||||
|
| dock-messages | 0.39 | 0.97 |
|
||||||
|
| dock-contacts | 0.61 | 0.97 |
|
||||||
|
| dock-browser | 0.84 | 0.97 |
|
||||||
|
| back-button | 0.10 | 0.05 |
|
||||||
|
|
||||||
|
## Full Navigation Test Sequence
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clear logs and run navigation test sequence
|
||||||
|
adb logcat -c
|
||||||
|
|
||||||
|
# Click Phone dock icon
|
||||||
|
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
|
||||||
|
--es touch_type "click" --ef x 0.16 --ef y 0.97
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Click back to return home
|
||||||
|
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
|
||||||
|
--es touch_type "click" --ef x 0.1 --ef y 0.05
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Click Messages dock icon
|
||||||
|
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
|
||||||
|
--es touch_type "click" --ef x 0.39 --ef y 0.97
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Click back to return home
|
||||||
|
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
|
||||||
|
--es touch_type "click" --ef x 0.1 --ef y 0.05
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Click Contacts dock icon
|
||||||
|
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
|
||||||
|
--es touch_type "click" --ef x 0.61 --ef y 0.97
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Click back to return home
|
||||||
|
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
|
||||||
|
--es touch_type "click" --ef x 0.1 --ef y 0.05
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Click Browser dock icon
|
||||||
|
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
|
||||||
|
--es touch_type "click" --ef x 0.84 --ef y 0.97
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reading Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Filter for Mosis logs
|
||||||
|
adb logcat -s MosisTest ServiceTester RMLUI
|
||||||
|
|
||||||
|
# Filter for navigation events
|
||||||
|
adb logcat -d | grep -iE "navigat|loaded|goBack|rml"
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
adb logcat -s MosisTest > mosis-log.txt
|
||||||
|
|
||||||
|
# Clear logs
|
||||||
|
adb logcat -c
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Log Output
|
||||||
|
|
||||||
|
Successful navigation shows these log patterns:
|
||||||
|
```
|
||||||
|
RMLUI: navigateTo called with: dialer
|
||||||
|
Loading screen: apps/dialer/dialer.rml
|
||||||
|
RMLUI: Navigated to: dialer (history depth: 1)
|
||||||
|
|
||||||
|
RMLUI: goBack called (history depth: 1)
|
||||||
|
Loading screen: apps/home/home.rml
|
||||||
|
RMLUI: Back to: home
|
||||||
|
```
|
||||||
99
docs/APP-MANAGEMENT.md
Normal file
99
docs/APP-MANAGEMENT.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# App Management System
|
||||||
|
|
||||||
|
The device-side app management system handles installation, updates, and launching of third-party apps.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ AppManager │
|
||||||
|
│ - Install/Uninstall apps from .mosis packages │
|
||||||
|
│ - Track installed apps in JSON registry │
|
||||||
|
│ - Manage app data/cache directories │
|
||||||
|
└─────────────────────────┬───────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ LuaSandboxManager │
|
||||||
|
│ - StartApp/StopApp lifecycle │
|
||||||
|
│ - Per-app isolated Lua environments │
|
||||||
|
│ - Resource limits (memory, CPU, timers) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## App Package Format (.mosis)
|
||||||
|
|
||||||
|
Apps are distributed as ZIP files with `.mosis` extension:
|
||||||
|
|
||||||
|
```
|
||||||
|
myapp.mosis
|
||||||
|
├── manifest.json # Required: app metadata
|
||||||
|
├── main.rml # Entry point (RmlUi document)
|
||||||
|
├── styles.rcss # Stylesheets
|
||||||
|
├── scripts/ # Lua scripts
|
||||||
|
│ └── app.lua
|
||||||
|
└── assets/ # Icons, images, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manifest Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "com.example.myapp",
|
||||||
|
"name": "My App",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_code": 1,
|
||||||
|
"entry": "main.rml",
|
||||||
|
"icon": "icon.tga",
|
||||||
|
"description": "App description",
|
||||||
|
"developer": {
|
||||||
|
"name": "Developer Name",
|
||||||
|
"email": "dev@example.com"
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"network",
|
||||||
|
"storage",
|
||||||
|
"camera"
|
||||||
|
],
|
||||||
|
"min_api_version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## App Lifecycle
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Install from local file
|
||||||
|
app_manager->InstallFromFile("/path/to/app.mosis", [](auto progress) {
|
||||||
|
LOG_INFO("Install progress: %s %.0f%%",
|
||||||
|
InstallProgress::StageName(progress.stage),
|
||||||
|
progress.progress * 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Launch app (starts sandbox)
|
||||||
|
app_manager->LaunchApp("com.example.myapp");
|
||||||
|
|
||||||
|
// Check if running
|
||||||
|
bool running = app_manager->IsAppRunning("com.example.myapp");
|
||||||
|
|
||||||
|
// Stop app (cleanup sandbox)
|
||||||
|
app_manager->StopApp("com.example.myapp");
|
||||||
|
|
||||||
|
// Uninstall (stops if running, removes files)
|
||||||
|
app_manager->Uninstall("com.example.myapp", false); // keep_data=false
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
/data/data/com.omixlab.mosis/files/
|
||||||
|
├── apps/
|
||||||
|
│ └── com.example.myapp/
|
||||||
|
│ ├── package/ # Extracted app files
|
||||||
|
│ ├── data/ # App persistent data (VirtualFS)
|
||||||
|
│ ├── cache/ # App cache (clearable)
|
||||||
|
│ └── db/ # SQLite databases
|
||||||
|
├── downloads/ # Temporary download location
|
||||||
|
├── backups/ # App data backups
|
||||||
|
└── config/
|
||||||
|
└── apps.json # Installed apps registry
|
||||||
|
```
|
||||||
105
docs/ARCHITECTURE.md
Normal file
105
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Architecture Overview
|
||||||
|
|
||||||
|
MosisService is an Android application combining Kotlin UI with native C++ libraries for UI rendering via Android's Binder IPC system.
|
||||||
|
|
||||||
|
## Two Native Libraries
|
||||||
|
|
||||||
|
**mosis-service** (`libmosis-service.so`):
|
||||||
|
- Main Android Binder service implementation
|
||||||
|
- Implements `IMosisService.aidl` interface for touch events and initialization
|
||||||
|
- Contains the Kernel rendering engine with RmlUi integration
|
||||||
|
- Links against RmlUi for HTML/CSS-like UI rendering
|
||||||
|
|
||||||
|
**mosis-test** (`libmosis-test.so`):
|
||||||
|
- Test/rendering client implementation
|
||||||
|
- Implements `IMosisListener.aidl` for receiving callbacks
|
||||||
|
- OpenGL ES 2.0 rendering pipeline using GLAD
|
||||||
|
|
||||||
|
## IPC Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Kotlin NativeService → JNI → mosis-service (IMosisService)
|
||||||
|
↓
|
||||||
|
IMosisListener callbacks
|
||||||
|
↓
|
||||||
|
mosis-test (rendering client)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Interfaces (AIDL)
|
||||||
|
|
||||||
|
`IMosisService`: `initOS()`, `onTouchDown()`, `onTouchMove()`, `onTouchUp()`
|
||||||
|
|
||||||
|
`IMosisListener` (oneway/async): `onServiceInitialized()`, `onBufferAvailable()`, `onFrameAvailable()`
|
||||||
|
|
||||||
|
## Multi-threading Model
|
||||||
|
|
||||||
|
- Service layer runs in main thread for event handling
|
||||||
|
- Rendering loop runs in dedicated thread managed by Kernel
|
||||||
|
- Async task processing for UI updates
|
||||||
|
- Thread-safe communication between components using std::mutex
|
||||||
|
|
||||||
|
## Rendering Pipeline
|
||||||
|
|
||||||
|
1. **Initialization**: Service connects to test layer, creates EGL context
|
||||||
|
2. **Buffer Management**: Hardware buffers allocated and shared between layers
|
||||||
|
3. **Event Processing**: Touch events processed and forwarded to Kernel
|
||||||
|
4. **Rendering Loop**: Continuous rendering with frame synchronization
|
||||||
|
5. **UI Updates**: RmlUi engine updates UI based on events and data
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
```
|
||||||
|
User Touch Events → IMosisService.onTouch* → Kernel.process → RmlUi UI Updates
|
||||||
|
Service Initialized → IMosisListener.onServiceInitialized → Rendering Setup
|
||||||
|
Buffer Available → IMosisListener.onBufferAvailable → Texture Creation
|
||||||
|
Frame Available → IMosisListener.onFrameAvailable → Frame Rendering
|
||||||
|
```
|
||||||
|
|
||||||
|
## Native Code Structure (src/main/cpp/)
|
||||||
|
|
||||||
|
**Core:**
|
||||||
|
- `kernel.cpp` - Core rendering engine, RmlUi integration, event processing
|
||||||
|
- `mosis-service.cpp` - Binder service implementation, JNI entry points
|
||||||
|
- `mosis-test.cpp` - Test client implementation
|
||||||
|
- `egl_context.cpp` - OpenGL ES context management
|
||||||
|
- `render_target.cpp` - Framebuffer and buffer management
|
||||||
|
- `RmlUi_Renderer_GL3.cpp` - RmlUi OpenGL renderer backend
|
||||||
|
- `assets_manager.cpp` - Android AssetManager integration
|
||||||
|
|
||||||
|
**App Management (`apps/`):**
|
||||||
|
- `app_manager.cpp` - App install/uninstall/launch lifecycle
|
||||||
|
- `app_api.cpp` - Lua API bindings for app management
|
||||||
|
- `update_service.cpp` - Background update checking
|
||||||
|
|
||||||
|
**Lua Sandbox (`sandbox/`):**
|
||||||
|
- `sandbox_manager.cpp` - Multi-app sandbox orchestrator
|
||||||
|
- `lua_sandbox.cpp` - Core Lua sandbox with resource limits
|
||||||
|
- `permission_gate.cpp` - Permission system (normal/dangerous/signature)
|
||||||
|
- `virtual_fs.cpp` - Per-app virtual filesystem with quotas
|
||||||
|
- `database_manager.cpp` - SQLite database per app
|
||||||
|
- `network_manager.cpp` - HTTP request validation
|
||||||
|
- `websocket_manager.cpp` - WebSocket connections
|
||||||
|
- `timer_manager.cpp` - setTimeout/setInterval implementation
|
||||||
|
- `json_api.cpp` - Safe JSON encode/decode
|
||||||
|
- `crypto_api.cpp` - Cryptographic functions (SHA256, HMAC)
|
||||||
|
- `camera_interface.cpp` - Camera access with indicators
|
||||||
|
- `microphone_interface.cpp` - Microphone access with indicators
|
||||||
|
- `audio_output.cpp` - Audio playback
|
||||||
|
- `location_interface.cpp` - GPS with precision reduction
|
||||||
|
- `sensor_interface.cpp` - Accelerometer, gyroscope, etc.
|
||||||
|
- `bluetooth_interface.cpp` - Bluetooth device access
|
||||||
|
- `contacts_interface.cpp` - Contacts read/write
|
||||||
|
- `message_bus.cpp` - Inter-app communication
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- C++23 standard with modern features (std::span, std::format)
|
||||||
|
- PascalCase for classes/functions, camelCase for variables
|
||||||
|
- RAII principles with smart pointers
|
||||||
|
- Kotlin code follows Android conventions
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- vcpkg manages native dependencies (RmlUi, GLFW, Freetype, Lua, libpng, nlohmann-json, minizip, sqlite3)
|
||||||
|
- CMake build system with vcpkg toolchain integration
|
||||||
|
- Android target architecture: arm64-v8a only
|
||||||
|
- Desktop target: Windows x64 (MSVC)
|
||||||
86
docs/BUILD-COMMANDS.md
Normal file
86
docs/BUILD-COMMANDS.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Build Commands
|
||||||
|
|
||||||
|
## Android (Gradle)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build entire project
|
||||||
|
./gradlew build
|
||||||
|
|
||||||
|
# Build debug APK
|
||||||
|
./gradlew assembleDebug
|
||||||
|
|
||||||
|
# Build release APK
|
||||||
|
./gradlew assembleRelease
|
||||||
|
|
||||||
|
# Clean build outputs
|
||||||
|
./gradlew clean
|
||||||
|
|
||||||
|
# Build with verbose output
|
||||||
|
./gradlew build --info --stacktrace
|
||||||
|
|
||||||
|
# Run lint checks
|
||||||
|
./gradlew lint
|
||||||
|
|
||||||
|
# Run unit tests
|
||||||
|
./gradlew test
|
||||||
|
|
||||||
|
# Run connected device tests
|
||||||
|
./gradlew connectedAndroidTest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Desktop Designer (CMake)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Configure (from designer/ folder)
|
||||||
|
cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake
|
||||||
|
|
||||||
|
# Build
|
||||||
|
cmake --build build --config Debug
|
||||||
|
|
||||||
|
# Run with hot-reload
|
||||||
|
./build/Debug/mosis-designer.exe ../src/main/assets/apps/home/home.rml
|
||||||
|
|
||||||
|
# Run with logging and hierarchy dump
|
||||||
|
./build/Debug/mosis-designer.exe ../src/main/assets/apps/home/home.rml --log output.log --hierarchy hierarchy.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Designer Tests (CMake)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Configure (from designer-test/ folder)
|
||||||
|
cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake
|
||||||
|
|
||||||
|
# Build
|
||||||
|
cmake --build build --config Debug
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
./build/Debug/designer-test.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sandbox Security Tests (CMake)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Configure (from sandbox-test/ folder)
|
||||||
|
cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake
|
||||||
|
|
||||||
|
# Build
|
||||||
|
cmake --build build --config Debug
|
||||||
|
|
||||||
|
# Run all tests (uber command)
|
||||||
|
./run_tests.bat
|
||||||
|
|
||||||
|
# Or run directly
|
||||||
|
./build/Debug/sandbox-test.exe
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
./build/Debug/sandbox-test.exe --test DangerousGlobals
|
||||||
|
./build/Debug/sandbox-test.exe --test Memory
|
||||||
|
./build/Debug/sandbox-test.exe --test CPU
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Requirements
|
||||||
|
|
||||||
|
Required environment variables:
|
||||||
|
- `ANDROID_HOME` - Android SDK path
|
||||||
|
- `ANDROID_NDK_HOME` - Android NDK path (version 29.0.14206865)
|
||||||
|
- `VCPKG_ROOT` - vcpkg package manager root
|
||||||
106
docs/CLAUDE.md
Normal file
106
docs/CLAUDE.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Company
|
||||||
|
|
||||||
|
**OmixLab LTD** - Package namespace: `com.omixlab`
|
||||||
|
|
||||||
|
## Git Commit Guidelines
|
||||||
|
|
||||||
|
**IMPORTANT**: When creating git commits:
|
||||||
|
- **DO NOT** add yourself as a co-author (no `Co-Authored-By` lines)
|
||||||
|
- **Commit messages must be a single line** - keep it concise and descriptive
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Mosis is a **virtual smartphone OS** for VR games and applications. It provides a phone-like device that users can interact with inside VR environments, with real smartphone functionality.
|
||||||
|
|
||||||
|
### Current Status
|
||||||
|
|
||||||
|
| Component | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| MosisService | ✅ Working | RmlUi rendering, touch input, navigation |
|
||||||
|
| App Management | ✅ Working | Install/uninstall apps, sandbox integration |
|
||||||
|
| Lua Sandbox | ✅ Working | 149 security tests passing |
|
||||||
|
| Desktop Designer | ✅ Working | Hot-reload, hierarchy dump, recording |
|
||||||
|
| Designer Tests | ✅ 5/5 Passing | Navigation tests automated |
|
||||||
|
| MosisVR (Unity) | ✅ Building | OpenGL backend working, Vulkan in progress |
|
||||||
|
| MosisUnreal | ✅ Working | Vulkan texture import via UE5 RHI, phone actor with mesh |
|
||||||
|
|
||||||
|
### Project Components
|
||||||
|
|
||||||
|
| Component | Location | Purpose |
|
||||||
|
|-----------|----------|---------|
|
||||||
|
| Android Service | `src/main/` | Native service running RmlUi renderer |
|
||||||
|
| App Management | `src/main/cpp/apps/` | App install/uninstall/launch with sandbox |
|
||||||
|
| Lua Sandbox | `src/main/cpp/sandbox/` | Per-app Lua isolation (22 modules) |
|
||||||
|
| Desktop Designer | `designer/` | UI development with hot-reload |
|
||||||
|
| Designer Tests | `designer-test/` | Automated UI testing framework |
|
||||||
|
| Sandbox Tests | `sandbox-test/` | Lua sandbox security tests (149 tests) |
|
||||||
|
| UI Assets | `src/main/assets/` | Shared RML/RCSS/Lua assets |
|
||||||
|
|
||||||
|
## Detailed Documentation
|
||||||
|
|
||||||
|
All detailed documentation is in `docs/`:
|
||||||
|
|
||||||
|
| Document | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| [BUILD-COMMANDS.md](BUILD-COMMANDS.md) | Android, Desktop Designer, and test build commands |
|
||||||
|
| [ARCHITECTURE.md](ARCHITECTURE.md) | Native libraries, IPC flow, code structure |
|
||||||
|
| [DESKTOP-DESIGNER.md](DESKTOP-DESIGNER.md) | Hot-reload, recording, key files |
|
||||||
|
| [TESTING-FRAMEWORK.md](TESTING-FRAMEWORK.md) | Automated UI testing, writing tests |
|
||||||
|
| [UI-ASSETS.md](UI-ASSETS.md) | Asset structure, navigation system, element IDs |
|
||||||
|
| [MATERIAL-DESIGN.md](MATERIAL-DESIGN.md) | Icons, MDL components, usage guide |
|
||||||
|
| [ANDROID-TESTING.md](ANDROID-TESTING.md) | ADB commands, event injection, logs |
|
||||||
|
| [GAME-ENGINES.md](GAME-ENGINES.md) | Unreal & Unity integration, Vulkan import |
|
||||||
|
| [DEVELOPER-PORTAL.md](DEVELOPER-PORTAL.md) | Portal architecture, milestones |
|
||||||
|
| [APP-MANAGEMENT.md](APP-MANAGEMENT.md) | Package format, manifest, lifecycle |
|
||||||
|
| [LUA-SANDBOX.md](LUA-SANDBOX.md) | Security features, permissions, APIs |
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Environment Requirements
|
||||||
|
|
||||||
|
- `ANDROID_HOME` - Android SDK path
|
||||||
|
- `ANDROID_NDK_HOME` - Android NDK path (version 29.0.14206865)
|
||||||
|
- `VCPKG_ROOT` - vcpkg package manager root
|
||||||
|
|
||||||
|
### Common Build Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Android APK
|
||||||
|
./gradlew assembleDebug
|
||||||
|
|
||||||
|
# Desktop Designer (from designer/)
|
||||||
|
cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake
|
||||||
|
cmake --build build --config Debug
|
||||||
|
|
||||||
|
# Sandbox Tests (from sandbox-test/)
|
||||||
|
./run_tests.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
- C++23 standard with modern features (std::span, std::format)
|
||||||
|
- PascalCase for classes/functions, camelCase for variables
|
||||||
|
- RAII principles with smart pointers
|
||||||
|
- Kotlin code follows Android conventions
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- vcpkg: RmlUi, GLFW, Freetype, Lua, libpng, nlohmann-json, minizip, sqlite3
|
||||||
|
- Android: arm64-v8a only
|
||||||
|
- Desktop: Windows x64 (MSVC)
|
||||||
|
|
||||||
|
## Documentation Guidelines
|
||||||
|
|
||||||
|
**IMPORTANT**: Always document progress and new commands to avoid rediscovery.
|
||||||
|
|
||||||
|
| Content Type | Location |
|
||||||
|
|--------------|----------|
|
||||||
|
| General concepts, architecture | `MosisService/docs/CLAUDE.md` or `docs/` |
|
||||||
|
| Unreal plugin docs | `MosisUnreal/Plugins/MosisSDK/README.md` |
|
||||||
|
| Unity package docs | `MosisVR/Packages/com.omixlab.mosis_sdk/README.md` |
|
||||||
|
|
||||||
|
**DO NOT** put documentation in the root `D:\Dev\Mosis\` directory - it is not versioned.
|
||||||
40
docs/DESKTOP-DESIGNER.md
Normal file
40
docs/DESKTOP-DESIGNER.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Desktop Designer
|
||||||
|
|
||||||
|
The desktop designer (`designer/`) provides rapid UI development with:
|
||||||
|
|
||||||
|
- **Hot-reload**: Automatically reloads when RML/RCSS/Lua files change
|
||||||
|
- **UI Hierarchy Dumping**: Exports element tree to JSON for inspection
|
||||||
|
- **Screenshot Capture**: PNG export via F12 key
|
||||||
|
- **Logging**: Detailed output for debugging navigation and events
|
||||||
|
- **Action Recording**: Record mouse/keyboard interactions to JSON
|
||||||
|
- **Action Playback**: Replay recorded interactions with timing
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `designer/src/main.cpp` | Main entry point, GLFW window, event loop |
|
||||||
|
| `designer/src/desktop_kernel.cpp` | RmlUi context management, rendering |
|
||||||
|
| `designer/src/testing/ui_inspector.cpp` | UI hierarchy JSON export |
|
||||||
|
| `designer/src/testing/visual_capture.cpp` | PNG screenshot capture and comparison |
|
||||||
|
| `designer/src/testing/action_recorder.cpp` | Record user interactions to JSON |
|
||||||
|
| `designer/src/testing/action_player.cpp` | Playback recorded actions |
|
||||||
|
| `designer/src/backend/RmlUi_Backend_GLFW_GL3.cpp` | GLFW backend with input hooks |
|
||||||
|
|
||||||
|
## Command Line Options
|
||||||
|
|
||||||
|
```
|
||||||
|
--log <path> Write logs to file
|
||||||
|
--hierarchy <path> Dump UI hierarchy JSON each frame
|
||||||
|
--dump Single-shot dump mode (screenshot + hierarchy)
|
||||||
|
--record <path> Enable recording mode (F5 to start/stop)
|
||||||
|
--playback <path> Play back recorded actions from JSON
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keyboard Controls
|
||||||
|
|
||||||
|
| Key | Function |
|
||||||
|
|-----|----------|
|
||||||
|
| F5 | Start/stop recording (when --record is enabled) |
|
||||||
|
| F6 | Pause/resume playback (when --playback is enabled) |
|
||||||
|
| F12 | Take screenshot |
|
||||||
56
docs/DEVELOPER-PORTAL.md
Normal file
56
docs/DEVELOPER-PORTAL.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Developer Portal
|
||||||
|
|
||||||
|
The Developer Portal is a web application for app developers to publish and manage their Mosis apps. Planning documents are in `DEV_PORTAL_M01-M12.md` files.
|
||||||
|
|
||||||
|
## Architecture Decisions
|
||||||
|
|
||||||
|
| Component | Technology | Rationale |
|
||||||
|
|-----------|------------|-----------|
|
||||||
|
| Backend | Go 1.22+ | Simple, fast, single binary deployment |
|
||||||
|
| Router | Chi | Lightweight, idiomatic Go HTTP routing |
|
||||||
|
| Database | SQLite + Litestream | Zero-ops, continuous backup to NAS storage |
|
||||||
|
| Frontend | htmx + Go templates | Server-rendered, minimal JS, fast |
|
||||||
|
| Storage | Synology NAS filesystem | Self-hosted, local volume mounts |
|
||||||
|
| Auth | OAuth2 (GitHub/Google) + JWT | golang-jwt for tokens, Ed25519 for signing |
|
||||||
|
| CLI | Go + Cobra | Cross-platform single binary |
|
||||||
|
| Docs | Hugo + Docsy | Static site served from /docs/ |
|
||||||
|
|
||||||
|
## Deployment Target
|
||||||
|
|
||||||
|
Self-hosted on Synology NAS via Docker:
|
||||||
|
|
||||||
|
```
|
||||||
|
/volume1/mosis/
|
||||||
|
├── data/
|
||||||
|
│ ├── portal.db # Main SQLite database
|
||||||
|
│ └── telemetry.db # Separate telemetry database
|
||||||
|
├── packages/ # Uploaded app packages
|
||||||
|
│ └── {dev_id}/{app_id}/{version}/
|
||||||
|
├── backups/ # Litestream replicas
|
||||||
|
└── docs/ # Hugo static site output
|
||||||
|
```
|
||||||
|
|
||||||
|
## Milestone Documents
|
||||||
|
|
||||||
|
| File | Topic | Status |
|
||||||
|
|------|-------|--------|
|
||||||
|
| DEV_PORTAL_M01_OVERVIEW.md | Project overview | Decided |
|
||||||
|
| DEV_PORTAL_M02_WEB_STACK.md | Go + Chi + htmx | Decided |
|
||||||
|
| DEV_PORTAL_M03_DATABASE.md | SQLite + Litestream | Decided |
|
||||||
|
| DEV_PORTAL_M04_AUTH.md | OAuth2 + JWT + Ed25519 | Decided |
|
||||||
|
| DEV_PORTAL_M05_FRONTEND.md | htmx + Go templates | Decided |
|
||||||
|
| DEV_PORTAL_M06_API.md | REST API design | Decided |
|
||||||
|
| DEV_PORTAL_M07_STORAGE.md | NAS filesystem | Decided |
|
||||||
|
| DEV_PORTAL_M08_TELEMETRY.md | SQLite + background workers | Decided |
|
||||||
|
| DEV_PORTAL_M09_REVIEW.md | Go validation workers | Decided |
|
||||||
|
| DEV_PORTAL_M10_DEVICE.md | C++ AppManager | Decided |
|
||||||
|
| DEV_PORTAL_M11_CLI.md | Go + Cobra CLI | Decided |
|
||||||
|
| DEV_PORTAL_M12_DOCS.md | Hugo + Docsy | Decided |
|
||||||
|
|
||||||
|
## Key Design Principles
|
||||||
|
|
||||||
|
1. **Single container** - Portal runs as one Docker container on NAS
|
||||||
|
2. **No external services** - SQLite, local filesystem, Pagefind search
|
||||||
|
3. **Pure Go** - `modernc.org/sqlite` (no CGO required)
|
||||||
|
4. **Server-rendered** - htmx for interactivity, no SPA framework
|
||||||
|
5. **Background workers** - Go goroutines for aggregation/cleanup jobs
|
||||||
348
docs/GAME-ENGINES.md
Normal file
348
docs/GAME-ENGINES.md
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
# Game Engine Integrations
|
||||||
|
|
||||||
|
MosisService provides a virtual phone that game engines can display and interact with.
|
||||||
|
|
||||||
|
## Integration Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ MosisService │
|
||||||
|
│ (OpenGL ES rendering → AHardwareBuffer) │
|
||||||
|
└───────────────────────────┬─────────────────────────────────────┘
|
||||||
|
│ Binder IPC + Shared Memory
|
||||||
|
┌─────────────────┴─────────────────┐
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────────────┐ ┌─────────────────────┐
|
||||||
|
│ MosisUnreal │ │ MosisVR │
|
||||||
|
│ (UE5.5 Plugin) │ │ (Unity Package) │
|
||||||
|
│ Vulkan Import │ │ Vulkan/OpenGL │
|
||||||
|
└─────────────────────┘ └─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## MosisUnreal (Unreal Engine 5.5)
|
||||||
|
|
||||||
|
**Location**: `D:\Dev\Mosis\MosisUnreal\Plugins\MosisSDK\`
|
||||||
|
|
||||||
|
### Build Commands
|
||||||
|
|
||||||
|
```batch
|
||||||
|
:: Windows Editor Build
|
||||||
|
"D:\Epic\UE_5.5\Engine\Build\BatchFiles\Build.bat" ^
|
||||||
|
MosisUnrealEditor Win64 Development ^
|
||||||
|
-Project="D:\Dev\Mosis\MosisUnreal\MosisUnreal.uproject"
|
||||||
|
|
||||||
|
:: Android APK Build
|
||||||
|
"D:\Epic\UE_5.5\Engine\Build\BatchFiles\RunUAT.bat" ^
|
||||||
|
BuildCookRun ^
|
||||||
|
-project="D:\Dev\Mosis\MosisUnreal\MosisUnreal.uproject" ^
|
||||||
|
-platform=Android -clientconfig=Development ^
|
||||||
|
-build -cook -stage -pak -package -noP4
|
||||||
|
|
||||||
|
:: Clean Build (delete these folders first)
|
||||||
|
rmdir /s /q "Intermediate\Build"
|
||||||
|
rmdir /s /q "Binaries"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output Files
|
||||||
|
|
||||||
|
| Build | Output |
|
||||||
|
|-------|--------|
|
||||||
|
| Windows Editor | `Plugins/MosisSDK/Binaries/Win64/UnrealEditor-MosisSDK.dll` |
|
||||||
|
| Android APK | `Binaries/Android/MosisUnreal-arm64.apk` |
|
||||||
|
| Android OBB | `Binaries/Android/main.1.com.omixlab.MosisUnreal.obb` |
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Android SDK Platform 36 (for AIDL binder headers)
|
||||||
|
- Android Build Tools 36.1.0 (for AIDL compiler)
|
||||||
|
- `ANDROID_HOME` environment variable set
|
||||||
|
|
||||||
|
### Quest Deployment
|
||||||
|
|
||||||
|
**IMPORTANT**: Git Bash on Windows converts Unix paths to Windows paths. Use `MSYS_NO_PATHCONV=1` prefix for all adb push commands.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set Quest device ID (check with: adb devices -l)
|
||||||
|
QUEST=2G0YC5ZF7W01T0
|
||||||
|
|
||||||
|
# Install APK
|
||||||
|
adb -s $QUEST install -r D:/Dev/Mosis/MosisUnreal/Binaries/Android/MosisUnreal-arm64.apk
|
||||||
|
|
||||||
|
# Create OBB temp directory
|
||||||
|
adb -s $QUEST shell "mkdir -p /data/local/tmp/obb/com.omixlab.MosisUnreal"
|
||||||
|
|
||||||
|
# Push OBB to temp location (MUST use MSYS_NO_PATHCONV=1)
|
||||||
|
MSYS_NO_PATHCONV=1 adb -s $QUEST push D:/Dev/Mosis/MosisUnreal/Binaries/Android/main.1.com.omixlab.MosisUnreal.obb /data/local/tmp/obb/com.omixlab.MosisUnreal/
|
||||||
|
|
||||||
|
# Move OBB to final location
|
||||||
|
adb -s $QUEST shell "rm -rf /sdcard/Android/obb/com.omixlab.MosisUnreal"
|
||||||
|
adb -s $QUEST shell "mv /data/local/tmp/obb/com.omixlab.MosisUnreal /sdcard/Android/obb/"
|
||||||
|
|
||||||
|
# Start MosisService first, then MosisUnreal
|
||||||
|
adb -s $QUEST shell am start -n com.omixlab.mosis/.MainActivity
|
||||||
|
sleep 3
|
||||||
|
adb -s $QUEST shell am start -n com.omixlab.MosisUnreal/com.epicgames.unreal.GameActivity
|
||||||
|
|
||||||
|
# Monitor logs
|
||||||
|
adb -s $QUEST logcat -s MosisSDK MosisOS MosisTest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Full rebuild and deploy sequence**:
|
||||||
|
```bash
|
||||||
|
# 1. Build Android APK+OBB
|
||||||
|
"D:\Epic\UE_5.5\Engine\Build\BatchFiles\RunUAT.bat" BuildCookRun \
|
||||||
|
-project="D:\Dev\Mosis\MosisUnreal\MosisUnreal.uproject" \
|
||||||
|
-platform=Android -clientconfig=Development \
|
||||||
|
-build -cook -stage -pak -package -noP4
|
||||||
|
|
||||||
|
# 2. Stop app, push APK and OBB, restart
|
||||||
|
QUEST=2G0YC5ZF7W01T0
|
||||||
|
adb -s $QUEST shell am force-stop com.omixlab.MosisUnreal
|
||||||
|
adb -s $QUEST install -r D:/Dev/Mosis/MosisUnreal/Binaries/Android/MosisUnreal-arm64.apk
|
||||||
|
adb -s $QUEST shell "mkdir -p /data/local/tmp/obb/com.omixlab.MosisUnreal"
|
||||||
|
MSYS_NO_PATHCONV=1 adb -s $QUEST push D:/Dev/Mosis/MosisUnreal/Binaries/Android/main.1.com.omixlab.MosisUnreal.obb /data/local/tmp/obb/com.omixlab.MosisUnreal/
|
||||||
|
adb -s $QUEST shell "rm -rf /sdcard/Android/obb/com.omixlab.MosisUnreal && mv /data/local/tmp/obb/com.omixlab.MosisUnreal /sdcard/Android/obb/"
|
||||||
|
adb -s $QUEST shell am start -n com.omixlab.MosisUnreal/com.epicgames.unreal.GameActivity
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common issues**:
|
||||||
|
- App stuck on loading screen: OBB not pushed or wrong version
|
||||||
|
- `adb push` path errors: Missing `MSYS_NO_PATHCONV=1` prefix
|
||||||
|
- Service not connecting: Start `com.omixlab.mosis` before the game
|
||||||
|
|
||||||
|
### VR Pointer Interaction
|
||||||
|
|
||||||
|
The MosisSDK includes `UMosisPointerComponent` for VR ray-based touch interaction.
|
||||||
|
|
||||||
|
**Key Files**:
|
||||||
|
- `Public/MosisPointerComponent.h` - Component header
|
||||||
|
- `Private/MosisPointerComponent.cpp` - Implementation
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Raycast from component transform (attach as child of motion controller)
|
||||||
|
- Automatic detection of `AMosisPhoneActor` hits
|
||||||
|
- Touch state machine: Down/Move/Up events
|
||||||
|
- Optional Enhanced Input binding via `TriggerAction` property
|
||||||
|
- Manual control via `SetTriggerPressed(bool)`
|
||||||
|
- Debug ray visualization
|
||||||
|
|
||||||
|
**Blueprint Setup**:
|
||||||
|
1. Add `MosisPointerComponent` as child of `MotionControllerComponent`
|
||||||
|
2. Set `TriggerAction` to your trigger input action (optional)
|
||||||
|
3. Touch events are sent automatically when pointing at phone and triggering
|
||||||
|
|
||||||
|
**C++ Setup**:
|
||||||
|
```cpp
|
||||||
|
// In VR Pawn header
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
UMosisPointerComponent* RightPointer;
|
||||||
|
|
||||||
|
// In constructor
|
||||||
|
RightPointer = CreateDefaultSubobject<UMosisPointerComponent>(TEXT("RightPointer"));
|
||||||
|
RightPointer->SetupAttachment(RightMotionController);
|
||||||
|
|
||||||
|
// In BeginPlay (if using Enhanced Input)
|
||||||
|
RightPointer->TriggerAction = TriggerInputAction;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manual Control (without Enhanced Input)**:
|
||||||
|
```cpp
|
||||||
|
// In your input handling code
|
||||||
|
void AMyVRPawn::OnTriggerPressed()
|
||||||
|
{
|
||||||
|
RightPointer->SetTriggerPressed(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AMyVRPawn::OnTriggerReleased()
|
||||||
|
{
|
||||||
|
RightPointer->SetTriggerPressed(false);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Component Properties**:
|
||||||
|
|
||||||
|
| Property | Type | Default | Description |
|
||||||
|
|----------|------|---------|-------------|
|
||||||
|
| `RayLength` | float | 500.0 | Maximum ray length in cm |
|
||||||
|
| `bShowDebugRay` | bool | false | Draw debug visualization |
|
||||||
|
| `DebugRayColor` | FLinearColor | Red | Ray color when not hitting |
|
||||||
|
| `DebugRayHitColor` | FLinearColor | Green | Ray color when hitting phone |
|
||||||
|
| `TraceChannel` | ECollisionChannel | Visibility | Collision channel for raycast |
|
||||||
|
| `TriggerAction` | UInputAction* | nullptr | Enhanced Input action for trigger |
|
||||||
|
|
||||||
|
**Blueprint Functions**:
|
||||||
|
|
||||||
|
| Function | Returns | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `IsPointingAtPhone()` | bool | True if ray hits a phone actor |
|
||||||
|
| `GetTargetPhone()` | AMosisPhoneActor* | Currently targeted phone (or nullptr) |
|
||||||
|
| `GetHitLocation()` | FVector | World location of ray hit |
|
||||||
|
| `GetRayOrigin()` | FVector | Ray start position |
|
||||||
|
| `GetRayDirection()` | FVector | Ray direction vector |
|
||||||
|
| `IsTriggerPressed()` | bool | Current trigger state |
|
||||||
|
| `SetTriggerPressed(bool)` | void | Manual trigger control |
|
||||||
|
|
||||||
|
**Touch State Machine**:
|
||||||
|
```
|
||||||
|
Idle ──[raycast hit]──► Hover ──[trigger]──► Pressed
|
||||||
|
▲ │ │
|
||||||
|
│ │ raycast miss │ trigger release
|
||||||
|
│ ▼ ▼
|
||||||
|
└──────────────────── Idle ◄────────────────────
|
||||||
|
|
||||||
|
Events:
|
||||||
|
- Idle → Pressed: SendTouch(Down)
|
||||||
|
- Pressed + moving: SendTouch(Move)
|
||||||
|
- Pressed → Idle: SendTouch(Up)
|
||||||
|
```
|
||||||
|
|
||||||
|
## MosisVR (Unity 6000.3.2f1)
|
||||||
|
|
||||||
|
**Location**: `D:\Dev\Mosis\MosisVR\Packages\com.omixlab.mosis_sdk\`
|
||||||
|
|
||||||
|
### Direct APK Build (Recommended)
|
||||||
|
|
||||||
|
```batch
|
||||||
|
"C:\Program Files\Unity\Hub\Editor\6000.3.2f1\Editor\Unity.exe" ^
|
||||||
|
-batchmode -quit -nographics ^
|
||||||
|
-projectPath "D:\Dev\Mosis\MosisVR" ^
|
||||||
|
-executeMethod BuildScript.BuildAndroidDirectCI ^
|
||||||
|
-outputPath "D:\Dev\Mosis\Builds\Unity\Android\MosisVR.apk"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export + Gradle Build
|
||||||
|
|
||||||
|
For more control, export a Gradle project then build separately:
|
||||||
|
|
||||||
|
```batch
|
||||||
|
:: Step 1: Export from Unity
|
||||||
|
"C:\Program Files\Unity\Hub\Editor\6000.3.2f1\Editor\Unity.exe" ^
|
||||||
|
-batchmode -quit -nographics ^
|
||||||
|
-projectPath "D:\Dev\Mosis\MosisVR" ^
|
||||||
|
-executeMethod BuildScript.BuildAndroidCI ^
|
||||||
|
-export true ^
|
||||||
|
-outputPath "D:\Dev\Mosis\Builds\Unity\Android\MosisVR"
|
||||||
|
|
||||||
|
:: Step 2: Build with Gradle
|
||||||
|
cd D:\Dev\Mosis\Builds\Unity\Android\MosisVR
|
||||||
|
gradle assembleRelease
|
||||||
|
:: APK at: launcher\build\outputs\apk\release\launcher-release.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unity Editor Manual Build
|
||||||
|
|
||||||
|
1. File > Build Settings > Android
|
||||||
|
2. Player Settings: IL2CPP, ARM64, Vulkan + OpenGLES3
|
||||||
|
3. For direct APK: Uncheck "Export Project", click Build
|
||||||
|
4. For export: Check "Export Project", click Export
|
||||||
|
|
||||||
|
### Native Plugin Build (Manual)
|
||||||
|
|
||||||
|
The native plugin builds automatically via CMake during Unity's build. To rebuild manually:
|
||||||
|
|
||||||
|
```batch
|
||||||
|
cd Packages/com.omixlab.mosis_sdk/Plugins/Android/cpp
|
||||||
|
cmake -B build -DCMAKE_TOOLCHAIN_FILE=%ANDROID_NDK_HOME%/build/cmake/android.toolchain.cmake ^
|
||||||
|
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-29
|
||||||
|
cmake --build build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Device Testing (Both Engines)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install MosisService first
|
||||||
|
adb install -r MosisService-debug.apk
|
||||||
|
|
||||||
|
# Install game client
|
||||||
|
adb install -r MosisUnreal-arm64.apk # or MosisVR.apk
|
||||||
|
|
||||||
|
# Launch service
|
||||||
|
adb shell am start -n com.omixlab.mosis/.MainActivity
|
||||||
|
|
||||||
|
# Launch client
|
||||||
|
adb shell am start -n com.omixlab.MosisUnreal/com.epicgames.unreal.GameActivity
|
||||||
|
# or for Unity:
|
||||||
|
adb shell am start -n com.omixlab.mosisvr/com.unity3d.player.UnityPlayerActivity
|
||||||
|
|
||||||
|
# Monitor all Mosis logs
|
||||||
|
adb logcat -s MosisSDK MosisTest RMLUI Vulkan
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vulkan HardwareBuffer Import
|
||||||
|
|
||||||
|
Both game engines use Vulkan to import AHardwareBuffer from MosisService.
|
||||||
|
|
||||||
|
### Required Vulkan Extensions
|
||||||
|
|
||||||
|
```
|
||||||
|
VK_ANDROID_external_memory_android_hardware_buffer
|
||||||
|
VK_KHR_external_memory
|
||||||
|
VK_KHR_dedicated_allocation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import Pattern
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 1. Query buffer properties
|
||||||
|
VkAndroidHardwareBufferPropertiesANDROID props = {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID
|
||||||
|
};
|
||||||
|
VkAndroidHardwareBufferFormatPropertiesANDROID formatProps = {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_FORMAT_PROPERTIES_ANDROID
|
||||||
|
};
|
||||||
|
props.pNext = &formatProps;
|
||||||
|
vkGetAndroidHardwareBufferPropertiesANDROID(device, buffer, &props);
|
||||||
|
|
||||||
|
// 2. Create image with external memory
|
||||||
|
VkExternalMemoryImageCreateInfo extInfo = {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO,
|
||||||
|
.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID
|
||||||
|
};
|
||||||
|
|
||||||
|
AHardwareBuffer_Desc desc;
|
||||||
|
AHardwareBuffer_describe(buffer, &desc);
|
||||||
|
|
||||||
|
VkImageCreateInfo imageInfo = {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
|
||||||
|
.pNext = &extInfo,
|
||||||
|
.imageType = VK_IMAGE_TYPE_2D,
|
||||||
|
.format = formatProps.format,
|
||||||
|
.extent = {desc.width, desc.height, 1},
|
||||||
|
.mipLevels = 1,
|
||||||
|
.arrayLayers = 1,
|
||||||
|
.samples = VK_SAMPLE_COUNT_1_BIT,
|
||||||
|
.tiling = VK_IMAGE_TILING_OPTIMAL,
|
||||||
|
.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||||
|
.sharingMode = VK_SHARING_MODE_EXCLUSIVE
|
||||||
|
};
|
||||||
|
vkCreateImage(device, &imageInfo, nullptr, &image);
|
||||||
|
|
||||||
|
// 3. Import memory from HardwareBuffer
|
||||||
|
VkImportAndroidHardwareBufferInfoANDROID importInfo = {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID,
|
||||||
|
.buffer = buffer
|
||||||
|
};
|
||||||
|
|
||||||
|
VkMemoryDedicatedAllocateInfo dedicatedInfo = {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO,
|
||||||
|
.pNext = &importInfo,
|
||||||
|
.image = image
|
||||||
|
};
|
||||||
|
|
||||||
|
VkMemoryAllocateInfo allocInfo = {
|
||||||
|
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
|
||||||
|
.pNext = &dedicatedInfo,
|
||||||
|
.allocationSize = props.allocationSize,
|
||||||
|
.memoryTypeIndex = FindMemoryType(props.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)
|
||||||
|
};
|
||||||
|
vkAllocateMemory(device, &allocInfo, nullptr, &memory);
|
||||||
|
vkBindImageMemory(device, image, memory, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Synchronization
|
||||||
|
|
||||||
|
Currently using CPU synchronization (`vkQueueWaitIdle` / `glFinish`). Future improvement: use Vulkan semaphores for GPU-GPU sync.
|
||||||
|
|
||||||
|
### Double Buffering
|
||||||
|
|
||||||
|
The imported image is copied to a local texture each frame to prevent data races with MosisService rendering.
|
||||||
126
docs/LUA-SANDBOX.md
Normal file
126
docs/LUA-SANDBOX.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Lua Sandbox System
|
||||||
|
|
||||||
|
The sandbox provides secure, isolated Lua environments for third-party apps.
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
| Feature | Implementation |
|
||||||
|
|---------|----------------|
|
||||||
|
| Dangerous globals removed | `os`, `io`, `loadfile`, `dofile`, `debug` |
|
||||||
|
| Memory limits | Configurable per-app (default 10MB) |
|
||||||
|
| CPU limits | Instruction counting with timeout |
|
||||||
|
| Bytecode rejected | Only source code allowed |
|
||||||
|
| Metatables protected | Cannot modify string/table metatables |
|
||||||
|
| Path traversal blocked | `../` and absolute paths rejected |
|
||||||
|
|
||||||
|
## Permission Categories
|
||||||
|
|
||||||
|
| Category | Auto-Grant | Examples |
|
||||||
|
|----------|------------|----------|
|
||||||
|
| Normal | Yes | `storage`, `network` |
|
||||||
|
| Dangerous | User consent | `camera`, `microphone`, `location`, `contacts` |
|
||||||
|
| Signature | System apps only | `system_settings`, `install_packages` |
|
||||||
|
|
||||||
|
## Available APIs
|
||||||
|
|
||||||
|
**Core APIs** (always available):
|
||||||
|
```lua
|
||||||
|
-- Timers
|
||||||
|
local id = setTimeout(function() end, 1000)
|
||||||
|
clearTimeout(id)
|
||||||
|
local id = setInterval(function() end, 500)
|
||||||
|
clearInterval(id)
|
||||||
|
|
||||||
|
-- JSON
|
||||||
|
local obj = json.decode('{"key": "value"}')
|
||||||
|
local str = json.encode({key = "value"})
|
||||||
|
|
||||||
|
-- Crypto
|
||||||
|
local bytes = crypto.randomBytes(16)
|
||||||
|
local hash = crypto.sha256("data")
|
||||||
|
local hmac = crypto.hmac("sha256", "key", "data")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Storage APIs** (requires `storage` permission):
|
||||||
|
```lua
|
||||||
|
-- Virtual filesystem (sandboxed to app directory)
|
||||||
|
fs.write("data.txt", "content")
|
||||||
|
local content = fs.read("data.txt")
|
||||||
|
local files = fs.list("/")
|
||||||
|
local stat = fs.stat("data.txt")
|
||||||
|
fs.delete("data.txt")
|
||||||
|
|
||||||
|
-- SQLite database
|
||||||
|
local db = database.open("mydb")
|
||||||
|
db:execute("CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT)")
|
||||||
|
db:execute("INSERT INTO items (name) VALUES (?)", {"item1"})
|
||||||
|
local rows = db:query("SELECT * FROM items WHERE id = ?", {1})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Network APIs** (requires `network` permission):
|
||||||
|
```lua
|
||||||
|
-- HTTP (HTTPS only, private IPs blocked)
|
||||||
|
local response = http.get("https://api.example.com/data")
|
||||||
|
local response = http.post("https://api.example.com/data", {
|
||||||
|
headers = {["Content-Type"] = "application/json"},
|
||||||
|
body = json.encode({key = "value"})
|
||||||
|
})
|
||||||
|
|
||||||
|
-- WebSocket
|
||||||
|
local ws = websocket.connect("wss://example.com/ws")
|
||||||
|
ws:send("message")
|
||||||
|
ws:onMessage(function(data) end)
|
||||||
|
ws:close()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hardware APIs** (requires dangerous permissions + user gesture):
|
||||||
|
```lua
|
||||||
|
-- Camera (requires camera permission)
|
||||||
|
camera.start(function(frame) end)
|
||||||
|
camera.stop()
|
||||||
|
|
||||||
|
-- Microphone (requires microphone permission)
|
||||||
|
microphone.start(function(samples) end)
|
||||||
|
microphone.stop()
|
||||||
|
|
||||||
|
-- Location (requires location permission)
|
||||||
|
location.getCurrentPosition(function(pos)
|
||||||
|
print(pos.latitude, pos.longitude)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Sensors (requires sensors permission)
|
||||||
|
sensors.subscribe("accelerometer", function(data)
|
||||||
|
print(data.x, data.y, data.z)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Sandbox Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd sandbox-test
|
||||||
|
cmake -B build -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake
|
||||||
|
cmake --build build --config Debug
|
||||||
|
./build/Debug/sandbox-test.exe
|
||||||
|
|
||||||
|
# Output: 149 tests, all passing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Categories
|
||||||
|
|
||||||
|
| Category | Tests | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| Security | 11 | Globals removal, bytecode, metatables |
|
||||||
|
| Resources | 8 | Memory, CPU limits, instruction counting |
|
||||||
|
| Permissions | 7 | Normal/dangerous/signature grants |
|
||||||
|
| Rate Limiting | 6 | API call throttling |
|
||||||
|
| Timers | 7 | setTimeout/setInterval behavior |
|
||||||
|
| JSON | 5 | Encode/decode, depth limits |
|
||||||
|
| Crypto | 4 | Random, SHA256, HMAC |
|
||||||
|
| VirtualFS | 8 | Read/write, quotas, traversal |
|
||||||
|
| Database | 8 | SQLite operations, injection prevention |
|
||||||
|
| Network | 8 | URL validation, private IP blocking |
|
||||||
|
| WebSocket | 7 | Connection limits, message size |
|
||||||
|
| Hardware | 42 | Camera, mic, location, sensors, bluetooth |
|
||||||
|
| IPC | 7 | Message bus between apps |
|
||||||
|
| Integration | 9 | Full app lifecycle |
|
||||||
|
| Fuzzing | 3 | Random input crash testing |
|
||||||
79
docs/MATERIAL-DESIGN.md
Normal file
79
docs/MATERIAL-DESIGN.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Material Design Resources
|
||||||
|
|
||||||
|
Material Design icons and components are available in the MosisDesigner repository.
|
||||||
|
|
||||||
|
## Material Design Icons
|
||||||
|
|
||||||
|
**Location**: `D:\Dev\Mosis\MosisDesigner\material-design-icons`
|
||||||
|
|
||||||
|
A comprehensive icon library from Google with 2000+ icons across 20 categories:
|
||||||
|
|
||||||
|
| Category | Examples |
|
||||||
|
|----------|----------|
|
||||||
|
| action | home, search, settings, delete, info |
|
||||||
|
| alert | error, warning, notification |
|
||||||
|
| av | play, pause, volume, mic |
|
||||||
|
| communication | phone, message, email, contacts |
|
||||||
|
| content | add, remove, copy, paste |
|
||||||
|
| device | battery, wifi, bluetooth, gps |
|
||||||
|
| editor | format, text, color, brush |
|
||||||
|
| file | folder, attachment, download, upload |
|
||||||
|
| hardware | keyboard, mouse, phone, tablet |
|
||||||
|
| home | lightbulb, thermostat, security |
|
||||||
|
| image | camera, photo, filter, tune |
|
||||||
|
| maps | location, directions, navigation |
|
||||||
|
| navigation | arrow, chevron, menu, close |
|
||||||
|
| notification | sync, update, event |
|
||||||
|
| places | hotel, restaurant, airport |
|
||||||
|
| search | search variants |
|
||||||
|
| social | share, person, group, notifications |
|
||||||
|
| toggle | star, checkbox, radio |
|
||||||
|
|
||||||
|
**Available Formats**:
|
||||||
|
- `src/` - SVG source files organized by category
|
||||||
|
- `png/` - PNG files at multiple DPIs (24dp, 36dp, 48dp)
|
||||||
|
- `font/` - Icon fonts (WOFF, TTF)
|
||||||
|
- `symbols/` - Material Symbols variable font (newer)
|
||||||
|
- `variablefont/` - Variable font files
|
||||||
|
|
||||||
|
**Icon Styles**:
|
||||||
|
- Outlined (default)
|
||||||
|
- Filled
|
||||||
|
- Rounded
|
||||||
|
- Sharp
|
||||||
|
- Two-tone (Material Icons only)
|
||||||
|
|
||||||
|
## Material Design Lite
|
||||||
|
|
||||||
|
**Location**: `D:\Dev\Mosis\MosisDesigner\material-design-lite`
|
||||||
|
|
||||||
|
CSS/JS component library implementing Material Design (reference implementation):
|
||||||
|
|
||||||
|
| Directory | Contents |
|
||||||
|
|-----------|----------|
|
||||||
|
| `src/` | SASS source for components |
|
||||||
|
| `docs/` | Component documentation |
|
||||||
|
| `templates/` | Page templates |
|
||||||
|
|
||||||
|
**Key Components** (for design reference):
|
||||||
|
- Buttons (raised, flat, FAB)
|
||||||
|
- Cards
|
||||||
|
- Dialogs
|
||||||
|
- Lists
|
||||||
|
- Menus
|
||||||
|
- Navigation drawers
|
||||||
|
- Progress indicators
|
||||||
|
- Sliders
|
||||||
|
- Snackbars
|
||||||
|
- Tables
|
||||||
|
- Tabs
|
||||||
|
- Text fields
|
||||||
|
- Tooltips
|
||||||
|
|
||||||
|
## Using Icons in Mosis
|
||||||
|
|
||||||
|
1. **Find icon** at https://fonts.google.com/icons
|
||||||
|
2. **Export SVG** from `material-design-icons/src/<category>/<name>/`
|
||||||
|
3. **Convert to TGA** using image tool (24x24 or 32x32, RGBA)
|
||||||
|
4. **Place in** `src/main/assets/icons/`
|
||||||
|
5. **Reference in RML**: `<img src="../../icons/<name>.tga"/>`
|
||||||
@@ -29,11 +29,11 @@ Mosis is a **virtual smartphone OS** for VR games and applications. It provides
|
|||||||
|---|-----------|--------|-------------|
|
|---|-----------|--------|-------------|
|
||||||
| 1 | Cross-Platform Kernel | ✅ Complete | Desktop designer with shared kernel code |
|
| 1 | Cross-Platform Kernel | ✅ Complete | Desktop designer with shared kernel code |
|
||||||
| 2 | Testing Framework | ✅ Complete | Automated UI testing and inspection |
|
| 2 | Testing Framework | ✅ Complete | Automated UI testing and inspection |
|
||||||
| 3 | Virtual Hardware | ❌ Not started | Camera, mic, speaker, filesystem APIs |
|
| 3 | Virtual Hardware | ✅ Complete | Camera, mic, speaker, filesystem APIs (via sandbox) |
|
||||||
| 4 | App Sandboxing | ❌ Not started | Lua/WASM runtime, package format |
|
| 4 | App Sandboxing | ✅ Complete | Lua runtime, package format, 149 security tests |
|
||||||
| 5 | WebRTC Bridge | ❌ Not started | Phone-to-phone communication |
|
| 5 | WebRTC Bridge | ❌ Not started | Phone-to-phone communication |
|
||||||
| 6 | System Apps | 🔶 75% | Core phone apps |
|
| 6 | System Apps | 🔶 75% | Core phone apps |
|
||||||
| 7 | Game Integration | ❌ Not started | Unity/Unreal plugin polish |
|
| 7 | Game Integration | 🔶 In Progress | Unity/Unreal plugins (Vulkan working) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -101,118 +101,48 @@ Mosis is a **virtual smartphone OS** for VR games and applications. It provides
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Milestone 3: Virtual Hardware ❌ NOT STARTED
|
## Milestone 3: Virtual Hardware ✅ COMPLETE
|
||||||
|
|
||||||
**Goal**: Hardware-like APIs backed by game engine or real devices.
|
**Goal**: Hardware-like APIs backed by game engine or real devices.
|
||||||
|
|
||||||
### 3.1 Camera Interface
|
Implemented as part of the Lua Sandbox system. See [SANDBOX_MILESTONES.md](SANDBOX_MILESTONES.md) for details.
|
||||||
|
|
||||||
```cpp
|
### Completed Components
|
||||||
class ICamera {
|
|
||||||
virtual void RequestFrame(FrameCallback callback) = 0;
|
|
||||||
virtual void SetResolution(int width, int height) = 0;
|
|
||||||
virtual bool IsAvailable() const = 0;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Implementations**:
|
| Component | Sandbox Milestone | Location |
|
||||||
- **Game Mode**: Receives texture from Unity/Unreal
|
|-----------|------------------|----------|
|
||||||
- **Desktop Mode**: System webcam (optional)
|
| Camera | Milestone 11 | `sandbox/camera_interface.cpp` |
|
||||||
- **Android Test Mode**: SharedTexture from MainActivity
|
| Microphone | Milestone 12 | `sandbox/microphone_interface.cpp` |
|
||||||
- **Mock Mode**: Test patterns
|
| Audio Output | Milestone 13 | `sandbox/audio_output.cpp` |
|
||||||
|
| Location/GPS | Milestone 14 | `sandbox/location_interface.cpp` |
|
||||||
### 3.2 Microphone Interface
|
| Sensors | Milestone 15 | `sandbox/sensor_interface.cpp` |
|
||||||
|
| Bluetooth | Milestone 16 | `sandbox/bluetooth_interface.cpp` |
|
||||||
```cpp
|
| Filesystem | Milestone 7 | `sandbox/virtual_fs.cpp` |
|
||||||
class IMicrophone {
|
| Network/HTTP | Milestone 9 | `sandbox/network_manager.cpp` |
|
||||||
virtual void StartCapture(AudioCallback callback) = 0;
|
| WebSocket | Milestone 10 | `sandbox/websocket_manager.cpp` |
|
||||||
virtual void StopCapture() = 0;
|
|
||||||
virtual bool IsAvailable() const = 0;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.3 Speaker Interface
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
class ISpeaker {
|
|
||||||
virtual void PlayAudio(AudioBuffer buffer) = 0;
|
|
||||||
virtual void SetVolume(float volume) = 0;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.4 Filesystem Interface
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
class IFileSystem {
|
|
||||||
virtual FileHandle Open(const std::string& path, Mode mode) = 0;
|
|
||||||
virtual std::vector<std::string> List(const std::string& dir) = 0;
|
|
||||||
virtual bool CreateDirectory(const std::string& path) = 0;
|
|
||||||
virtual bool Delete(const std::string& path) = 0;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.5 Network Interface
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
class INetwork {
|
|
||||||
virtual HttpResponse Fetch(const HttpRequest& request) = 0;
|
|
||||||
virtual WebSocket Connect(const std::string& url) = 0;
|
|
||||||
virtual bool IsOnline() const = 0;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Milestone 4: App Sandboxing ❌ NOT STARTED
|
## Milestone 4: App Sandboxing ✅ COMPLETE
|
||||||
|
|
||||||
**Goal**: Secure app runtime with defined package format.
|
**Goal**: Secure app runtime with defined package format.
|
||||||
|
|
||||||
### 4.1 Runtime Decision
|
Implemented with 20 sub-milestones covering security, APIs, and hardware interfaces. 149 security tests passing.
|
||||||
|
|
||||||
| Aspect | Lua | WASM |
|
See:
|
||||||
|--------|-----|------|
|
- [SANDBOX_MILESTONES.md](SANDBOX_MILESTONES.md) - Implementation milestones
|
||||||
| Isolation | Weak | Strong |
|
- [LUA-SANDBOX.md](LUA-SANDBOX.md) - Security features, permissions, APIs
|
||||||
| Performance | Good for UI | Near-native |
|
- [APP-MANAGEMENT.md](APP-MANAGEMENT.md) - Package format, manifest, lifecycle
|
||||||
| RmlUi Integration | Native | Needs bridge |
|
|
||||||
| Ecosystem | Small | Large |
|
|
||||||
|
|
||||||
**Recommendation**: Hybrid approach
|
### Key Features
|
||||||
- Lua for UI scripting (RmlUi integration)
|
|
||||||
- WASM for app logic needing isolation (future)
|
|
||||||
|
|
||||||
### 4.2 Package Format (.mpkg)
|
- **Lua Runtime**: Isolated `lua_State` per app with dangerous globals removed
|
||||||
|
- **Resource Limits**: Memory (16MB), CPU (instruction hooks), timers (100 max)
|
||||||
```
|
- **Permission System**: Normal/Dangerous/Signature categories with user gesture requirements
|
||||||
myapp.mpkg/
|
- **Virtual Filesystem**: Per-app storage with 50MB quota
|
||||||
├── manifest.json # Metadata, permissions, entry
|
- **SQLite Database**: Per-app with authorizer blocking dangerous operations
|
||||||
├── ui/
|
- **Network Security**: HTTPS required, private IP blocking, domain whitelists
|
||||||
│ ├── main.rml
|
- **Hardware Interfaces**: Camera, mic, location, sensors with mandatory indicators
|
||||||
│ ├── styles.rcss
|
|
||||||
│ └── scripts/
|
|
||||||
├── assets/
|
|
||||||
│ ├── icons/
|
|
||||||
│ └── images/
|
|
||||||
└── data/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 Manifest Schema
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "com.example.myapp",
|
|
||||||
"name": "My App",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"permissions": ["camera", "network", "storage"],
|
|
||||||
"entry": "ui/main.rml",
|
|
||||||
"icon": "assets/icons/app.png"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.4 App Lifecycle
|
|
||||||
|
|
||||||
```
|
|
||||||
INSTALLED → LAUNCHING → RUNNING → PAUSED → STOPPED → UNINSTALLED
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -420,24 +350,22 @@ cmake --build build --config Debug
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Current Sprint: Complete Partial Tasks
|
## Current Sprint: Next Steps
|
||||||
|
|
||||||
### Priority 1: Testing Framework Completion ✅ DONE
|
### Priority 1: System Apps Completion
|
||||||
- [x] Action recording (capture interactions to JSON)
|
|
||||||
- [x] Action playback (replay with timing)
|
|
||||||
- [x] Screenshot diff (visual regression)
|
|
||||||
- [x] GLFW input hooks for automatic recording
|
|
||||||
|
|
||||||
### Priority 2: Remaining System Apps
|
|
||||||
- [ ] Store app (UI only - browse, install)
|
- [ ] Store app (UI only - browse, install)
|
||||||
- [ ] Camera app (UI + shared texture from MainActivity)
|
- [ ] Camera app (UI + game engine texture)
|
||||||
- [ ] Music app (UI outline only)
|
- [ ] Music app (UI outline only)
|
||||||
|
|
||||||
### Priority 3: App Data Persistence
|
### Priority 2: Game Engine Integration
|
||||||
- [ ] JSON/SQLite storage layer
|
- [x] MosisUnreal - Vulkan texture import working
|
||||||
- [ ] Contact CRUD operations
|
- [ ] MosisUnreal - Blueprint API polish
|
||||||
- [ ] Message history
|
- [ ] MosisVR (Unity) - Vulkan backend
|
||||||
- [ ] Settings persistence
|
|
||||||
|
### Priority 3: WebRTC Bridge (Milestone 5)
|
||||||
|
- [ ] libdatachannel integration
|
||||||
|
- [ ] Phone-to-phone communication
|
||||||
|
- [ ] Real smartphone bridge app
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -454,10 +382,11 @@ cmake --build build --config Debug
|
|||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
|
All documentation is in `docs/`:
|
||||||
- `CLAUDE.md` - Development guide
|
- `CLAUDE.md` - Development guide
|
||||||
- `TESTING.md` - Testing framework documentation
|
- `SANDBOX_MILESTONES.md` - Sandbox implementation details
|
||||||
- `ROADMAP.md` - This file
|
- `ROADMAP.md` - This file
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Last updated: 2026-01-16*
|
*Last updated: 2026-01-19*
|
||||||
178
docs/SANDBOX.md
Normal file
178
docs/SANDBOX.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# Mosis Lua Sandbox Security
|
||||||
|
|
||||||
|
**Status**: ✅ Complete (149 security tests passing)
|
||||||
|
**Goal**: Secure app isolation with defense-in-depth approach.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Third-party apps run in isolated Lua environments with restricted access to system resources. Each app gets its own `lua_State` with carefully controlled APIs.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Mosis Kernel │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ LuaSandboxManager │ │
|
||||||
|
│ │ - Creates per-app lua_State with custom allocator │ │
|
||||||
|
│ │ - Enforces memory/CPU limits │ │
|
||||||
|
│ │ - Routes permission-gated API calls │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ App A State │ │ App B State │ │ App C State │ │
|
||||||
|
│ │ (isolated) │ │ (isolated) │ │ (isolated) │ │
|
||||||
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └───────────────────┴───────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────▼────────┐ │
|
||||||
|
│ │ Permission Gate │ │
|
||||||
|
│ └────────┬────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────────────────────▼──────────────────────────────────┐ │
|
||||||
|
│ │ System Services │ │
|
||||||
|
│ │ Camera │ Network │ Storage │ Contacts │ Messages │ ... │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Threat Model
|
||||||
|
|
||||||
|
### Threat Categories
|
||||||
|
|
||||||
|
| Category | Threat | Severity | Mitigation |
|
||||||
|
|----------|--------|----------|------------|
|
||||||
|
| **Code Execution** | `os.execute()`, `io.popen()` | Critical | Remove globals |
|
||||||
|
| **File Access** | `io.open()`, `loadfile()` | Critical | Remove globals |
|
||||||
|
| **Bytecode Injection** | `load()` with binary chunks | Critical | Text-only loading |
|
||||||
|
| **Memory Exhaustion** | Infinite tables/strings | High | Custom allocator with limits |
|
||||||
|
| **CPU Exhaustion** | Infinite loops | High | Instruction count hook |
|
||||||
|
| **Sandbox Escape** | `debug.getregistry()` | Critical | Remove debug library |
|
||||||
|
| **Global Pollution** | Modifying `_G`, `string` | Medium | Frozen globals |
|
||||||
|
| **Path Traversal** | `require("../../etc/passwd")` | Critical | Path validation |
|
||||||
|
| **Data Exfiltration** | Unauthorized network access | High | Permission-gated network |
|
||||||
|
| **Privilege Escalation** | Access other app's data | High | Per-app storage isolation |
|
||||||
|
| **Timing Attacks** | High-resolution timers | Low | Limit timer precision |
|
||||||
|
| **Side Channels** | Memory/CPU usage patterns | Low | Rate limiting |
|
||||||
|
|
||||||
|
### Attacker Capabilities
|
||||||
|
|
||||||
|
We assume a malicious app developer can:
|
||||||
|
- Write arbitrary Lua code within the sandbox
|
||||||
|
- Attempt to exploit any exposed API
|
||||||
|
- Try to escape the sandbox via Lua language features
|
||||||
|
- Attempt DoS via resource exhaustion
|
||||||
|
- Try to access other apps' data
|
||||||
|
- Attempt to exfiltrate user data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Layers
|
||||||
|
|
||||||
|
### Layer 1: Dangerous Globals Removal
|
||||||
|
Remove all dangerous functions before any app code runs: `os`, `io`, `debug`, `package`, `require`, `ffi`, `jit`, `dofile`, `loadfile`, `load`, `loadstring`, `rawget`, `rawset`, `collectgarbage`, `string.dump`.
|
||||||
|
|
||||||
|
### Layer 2: Bytecode Prevention
|
||||||
|
Only allow text chunks (`"t"` mode), reject binary Lua bytecode.
|
||||||
|
|
||||||
|
### Layer 3: Memory Limits
|
||||||
|
Custom allocator tracks and limits memory per app (16 MB default).
|
||||||
|
|
||||||
|
### Layer 4: CPU Limits
|
||||||
|
Instruction count hook interrupts runaway code (10M instructions default).
|
||||||
|
|
||||||
|
### Layer 5: Metatable Protection
|
||||||
|
Freeze `_G` and string metatable to prevent modification.
|
||||||
|
|
||||||
|
### Layer 6: Permission System
|
||||||
|
Three categories: Normal (auto-granted), Dangerous (user prompt), Signature (system only).
|
||||||
|
|
||||||
|
### Layer 7: Rate Limiting
|
||||||
|
Token bucket rate limiting on sensitive operations.
|
||||||
|
|
||||||
|
### Layer 8: Audit Logging
|
||||||
|
All security events logged for forensics.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
The sandbox is implemented across 20 milestones with 22 modules. See:
|
||||||
|
|
||||||
|
- **[SANDBOX_MILESTONES.md](SANDBOX_MILESTONES.md)** - Complete implementation details for all 20 milestones
|
||||||
|
- **[LUA-SANDBOX.md](LUA-SANDBOX.md)** - API documentation for app developers
|
||||||
|
|
||||||
|
### Milestone Overview
|
||||||
|
|
||||||
|
| Phase | Milestones | Components |
|
||||||
|
|-------|------------|------------|
|
||||||
|
| **Foundation** | 1-4 | Core sandbox, permissions, audit logging, path security |
|
||||||
|
| **Core APIs** | 5-8 | Timers, JSON, crypto, virtual filesystem, SQLite |
|
||||||
|
| **Network** | 9-10 | HTTP requests, WebSocket connections |
|
||||||
|
| **Hardware** | 11-17 | Camera, mic, audio, location, sensors, Bluetooth, contacts |
|
||||||
|
| **System** | 18-20 | Inter-app messaging, security tests, kernel integration |
|
||||||
|
|
||||||
|
### Source Files
|
||||||
|
|
||||||
|
All sandbox code is in `src/main/cpp/sandbox/`:
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `sandbox_manager.cpp` | Multi-app orchestrator |
|
||||||
|
| `lua_sandbox.cpp` | Core Lua sandbox with resource limits |
|
||||||
|
| `permission_gate.cpp` | Permission system |
|
||||||
|
| `virtual_fs.cpp` | Per-app virtual filesystem |
|
||||||
|
| `database_manager.cpp` | SQLite per app |
|
||||||
|
| `network_manager.cpp` | HTTP request validation |
|
||||||
|
| `websocket_manager.cpp` | WebSocket connections |
|
||||||
|
| `timer_manager.cpp` | setTimeout/setInterval |
|
||||||
|
| `json_api.cpp` | Safe JSON encode/decode |
|
||||||
|
| `crypto_api.cpp` | SHA256, HMAC, secure random |
|
||||||
|
| `camera_interface.cpp` | Camera with indicators |
|
||||||
|
| `microphone_interface.cpp` | Microphone with indicators |
|
||||||
|
| `audio_output.cpp` | Audio playback |
|
||||||
|
| `location_interface.cpp` | GPS with precision control |
|
||||||
|
| `sensor_interface.cpp` | Accelerometer, gyroscope, etc. |
|
||||||
|
| `bluetooth_interface.cpp` | Bluetooth device access |
|
||||||
|
| `contacts_interface.cpp` | Contacts read/write |
|
||||||
|
| `message_bus.cpp` | Inter-app communication |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
149 security tests verify sandbox integrity. Run with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd sandbox-test
|
||||||
|
./run_tests.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Categories
|
||||||
|
|
||||||
|
- Dangerous globals removal
|
||||||
|
- Bytecode rejection
|
||||||
|
- Memory limit enforcement
|
||||||
|
- CPU limit enforcement
|
||||||
|
- Metatable protection
|
||||||
|
- Path traversal prevention
|
||||||
|
- Permission checks
|
||||||
|
- Rate limiting
|
||||||
|
- Network security (private IP blocking, HTTPS enforcement)
|
||||||
|
- Hardware indicator requirements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Lua 5.4 Manual - Sandboxing](https://www.lua.org/manual/5.4/)
|
||||||
|
- [OWASP Secure Coding Practices](https://owasp.org/www-project-secure-coding-practices-quick-reference-guide/)
|
||||||
|
- Android permission model
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 2026-01-19*
|
||||||
60
docs/TESTING-FRAMEWORK.md
Normal file
60
docs/TESTING-FRAMEWORK.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Automated Testing Framework
|
||||||
|
|
||||||
|
The designer-test (`designer-test/`) provides automated UI testing.
|
||||||
|
|
||||||
|
## Test Architecture
|
||||||
|
|
||||||
|
1. **WindowController**: Finds designer window, sends mouse/keyboard events via Windows SendInput API
|
||||||
|
2. **HierarchyReader**: Parses UI hierarchy JSON to find elements by ID/class (with retry logic and exponential backoff)
|
||||||
|
3. **LogParser**: Monitors log file for navigation events
|
||||||
|
4. **TestRunner**: Orchestrates test execution, reports results
|
||||||
|
5. **UIInspector**: Dumps UI hierarchy with atomic writes (temp file + rename pattern)
|
||||||
|
|
||||||
|
## Key Implementation Details
|
||||||
|
|
||||||
|
- **Path Normalization**: RmlUi uses `|` instead of `:` in Windows paths (e.g., `D|\Dev\...`). The UIInspector normalizes paths for correct document matching.
|
||||||
|
- **Atomic File Writes**: Hierarchy files are written to `.tmp` then renamed to prevent partial reads.
|
||||||
|
- **Retry with Backoff**: HierarchyReader retries up to 10 times with exponential backoff (30ms base) and validates JSON completeness.
|
||||||
|
- **Dynamic Back Button**: `GoHome()` finds back buttons from hierarchy by class (`app-bar-nav` or `browser-nav-btn`) instead of fixed coordinates.
|
||||||
|
|
||||||
|
## Writing Tests
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Find element by ID and click it
|
||||||
|
bool ClickById(TestContext& ctx, const std::string& id) {
|
||||||
|
ctx.hierarchy.Reload();
|
||||||
|
auto element = ctx.hierarchy.FindById(id);
|
||||||
|
if (!element) return false;
|
||||||
|
|
||||||
|
int x = element->bounds.centerX();
|
||||||
|
int y = element->bounds.centerY();
|
||||||
|
ScaleToPhysical(ctx, x, y); // Convert logical to physical coords
|
||||||
|
ctx.window.SendClick(x, y);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test example
|
||||||
|
bool TestNavigateToDialer(TestContext& ctx) {
|
||||||
|
GoHome(ctx);
|
||||||
|
ctx.log.Clear();
|
||||||
|
|
||||||
|
if (!ClickById(ctx, "dock-phone")) return false;
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||||
|
ctx.log.Reload();
|
||||||
|
return ctx.log.Contains("Loaded screen: apps/dialer/dialer.rml");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Output
|
||||||
|
|
||||||
|
Tests produce JSON results at `test_results.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Mosis Designer UI Tests",
|
||||||
|
"summary": {"passed": 5, "failed": 0, "total": 5},
|
||||||
|
"tests": [
|
||||||
|
{"name": "Navigate to Dialer", "status": "passed", "duration_ms": 3500}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
67
docs/UI-ASSETS.md
Normal file
67
docs/UI-ASSETS.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# UI Assets Structure
|
||||||
|
|
||||||
|
All UI assets are in `src/main/assets/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
assets/
|
||||||
|
├── apps/ # System apps (RML documents)
|
||||||
|
│ ├── home/home.rml # Home screen launcher
|
||||||
|
│ ├── dialer/dialer.rml # Phone dialer
|
||||||
|
│ ├── messages/ # Messages app
|
||||||
|
│ ├── contacts/ # Contacts app
|
||||||
|
│ ├── settings/ # Settings app
|
||||||
|
│ └── browser/ # Web browser
|
||||||
|
├── ui/ # Shared stylesheets
|
||||||
|
│ ├── html.rcss # Base HTML element styles
|
||||||
|
│ ├── theme.rcss # Design tokens (colors, typography)
|
||||||
|
│ └── components.rcss # Reusable UI components
|
||||||
|
├── scripts/ # Lua scripts
|
||||||
|
│ └── navigation.lua # Navigation system
|
||||||
|
├── icons/ # TGA icon files (24x24, 32x32)
|
||||||
|
└── fonts/ # TTF fonts (LatoLatin, Roboto)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Navigation System
|
||||||
|
|
||||||
|
Navigation is handled by `scripts/navigation.lua`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Navigate to a screen
|
||||||
|
navigateTo('dialer') -- Push to history, animate forward
|
||||||
|
|
||||||
|
-- Go back
|
||||||
|
goBack() -- Pop from history, animate back
|
||||||
|
|
||||||
|
-- Go home
|
||||||
|
goHome() -- Clear history, return to home
|
||||||
|
```
|
||||||
|
|
||||||
|
## Element IDs for Testing
|
||||||
|
|
||||||
|
Key elements have IDs for automated testing:
|
||||||
|
|
||||||
|
| ID | Location | Purpose |
|
||||||
|
|----|----------|---------|
|
||||||
|
| `dock-phone` | home.rml | Phone dock icon |
|
||||||
|
| `dock-messages` | home.rml | Messages dock icon |
|
||||||
|
| `dock-contacts` | home.rml | Contacts dock icon |
|
||||||
|
| `dock-browser` | home.rml | Browser dock icon |
|
||||||
|
| `app-settings` | home.rml | Settings app icon |
|
||||||
|
|
||||||
|
## CSS Classes for Navigation
|
||||||
|
|
||||||
|
Back buttons use `app-bar-nav` class for automated GoHome:
|
||||||
|
```html
|
||||||
|
<div class="app-bar-nav btn-icon" onclick="goBack()">
|
||||||
|
<img src="../../icons/back.tga"/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Browser uses `browser-nav-btn` class for its toolbar back button:
|
||||||
|
```html
|
||||||
|
<div class="app-bar-nav browser-nav-btn" onclick="goBack()">
|
||||||
|
<img src="../../icons/back.tga"/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
The test framework's `FindBackButton()` searches for both classes to handle all screen layouts.
|
||||||
BIN
test-apps/com.mosis.sandbox-test.mosis
Normal file
BIN
test-apps/com.mosis.sandbox-test.mosis
Normal file
Binary file not shown.
211
test-apps/com.mosis.sandbox-test/app.lua
Normal file
211
test-apps/com.mosis.sandbox-test/app.lua
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
-- Sandbox Test App
|
||||||
|
-- Tests: timers, JSON, crypto, storage
|
||||||
|
|
||||||
|
local results = {}
|
||||||
|
local logCounter = 0
|
||||||
|
|
||||||
|
local function log(msg)
|
||||||
|
logCounter = logCounter + 1
|
||||||
|
table.insert(results, string.format("[%03d] %s", logCounter, msg))
|
||||||
|
-- document may not be available during initial script load
|
||||||
|
if document then
|
||||||
|
local el = document:GetElementById("results")
|
||||||
|
if el then
|
||||||
|
el.inner_rml = table.concat(results, "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Navigation helper
|
||||||
|
function goBack()
|
||||||
|
if navigation and navigation.back then
|
||||||
|
navigation.back()
|
||||||
|
else
|
||||||
|
log("Navigation not available")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function setStatus(id, status, success)
|
||||||
|
-- document may not be available during initial script load
|
||||||
|
if not document then return end
|
||||||
|
local el = document:GetElementById(id)
|
||||||
|
if el then
|
||||||
|
if success then
|
||||||
|
el.inner_rml = "✓ " .. status
|
||||||
|
else
|
||||||
|
el.inner_rml = "✗ " .. status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Timer test
|
||||||
|
function testTimer()
|
||||||
|
setStatus("timer-status", "Running...", true)
|
||||||
|
log("Starting timer test...")
|
||||||
|
|
||||||
|
local count = 0
|
||||||
|
local timerId = nil
|
||||||
|
|
||||||
|
timerId = setInterval(function()
|
||||||
|
count = count + 1
|
||||||
|
log("Timer tick: " .. count)
|
||||||
|
|
||||||
|
if count >= 3 then
|
||||||
|
clearInterval(timerId)
|
||||||
|
setStatus("timer-status", "Passed (3 ticks)", true)
|
||||||
|
log("Timer test complete!")
|
||||||
|
end
|
||||||
|
end, 1000)
|
||||||
|
|
||||||
|
log("Timer started with ID: " .. tostring(timerId))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- JSON test
|
||||||
|
function testJSON()
|
||||||
|
log("Starting JSON test...")
|
||||||
|
|
||||||
|
local success = true
|
||||||
|
local msg = ""
|
||||||
|
|
||||||
|
-- Test encode
|
||||||
|
local data = {
|
||||||
|
name = "test",
|
||||||
|
value = 42,
|
||||||
|
nested = { a = 1, b = 2 }
|
||||||
|
}
|
||||||
|
|
||||||
|
local encoded = json.encode(data)
|
||||||
|
if encoded then
|
||||||
|
log("Encoded: " .. encoded)
|
||||||
|
else
|
||||||
|
success = false
|
||||||
|
msg = "encode failed"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test decode
|
||||||
|
if success then
|
||||||
|
local decoded = json.decode(encoded)
|
||||||
|
if decoded and decoded.name == "test" and decoded.value == 42 then
|
||||||
|
log("Decoded successfully, name=" .. decoded.name)
|
||||||
|
else
|
||||||
|
success = false
|
||||||
|
msg = "decode failed"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if success then
|
||||||
|
setStatus("json-status", "Passed", true)
|
||||||
|
log("JSON test complete!")
|
||||||
|
else
|
||||||
|
setStatus("json-status", "Failed: " .. msg, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Crypto test
|
||||||
|
function testCrypto()
|
||||||
|
log("Starting crypto test...")
|
||||||
|
|
||||||
|
local success = true
|
||||||
|
local msg = ""
|
||||||
|
|
||||||
|
-- Test random bytes
|
||||||
|
local bytes = crypto.randomBytes(16)
|
||||||
|
if bytes and #bytes == 16 then
|
||||||
|
log("Random bytes (hex): " .. bytes:gsub(".", function(c)
|
||||||
|
return string.format("%02x", c:byte())
|
||||||
|
end))
|
||||||
|
else
|
||||||
|
success = false
|
||||||
|
msg = "randomBytes failed"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test SHA256
|
||||||
|
if success then
|
||||||
|
local hash = crypto.sha256("hello world")
|
||||||
|
if hash then
|
||||||
|
log("SHA256: " .. hash:sub(1, 32) .. "...")
|
||||||
|
else
|
||||||
|
success = false
|
||||||
|
msg = "sha256 failed"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test HMAC
|
||||||
|
if success then
|
||||||
|
local hmac = crypto.hmac("sha256", "secret", "message")
|
||||||
|
if hmac then
|
||||||
|
log("HMAC: " .. hmac:sub(1, 32) .. "...")
|
||||||
|
else
|
||||||
|
success = false
|
||||||
|
msg = "hmac failed"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if success then
|
||||||
|
setStatus("crypto-status", "Passed", true)
|
||||||
|
log("Crypto test complete!")
|
||||||
|
else
|
||||||
|
setStatus("crypto-status", "Failed: " .. msg, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Storage test
|
||||||
|
function testStorage()
|
||||||
|
log("Starting storage test...")
|
||||||
|
|
||||||
|
local success = true
|
||||||
|
local msg = ""
|
||||||
|
|
||||||
|
-- Test write (VirtualFS requires /data/, /cache/, /temp/, or /shared/ prefix)
|
||||||
|
local writeOk = fs.write("/data/test.txt", "Hello from sandbox!")
|
||||||
|
if writeOk then
|
||||||
|
log("Write successful")
|
||||||
|
else
|
||||||
|
success = false
|
||||||
|
msg = "write failed"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test read
|
||||||
|
if success then
|
||||||
|
local content = fs.read("/data/test.txt")
|
||||||
|
if content == "Hello from sandbox!" then
|
||||||
|
log("Read successful: " .. content)
|
||||||
|
else
|
||||||
|
success = false
|
||||||
|
msg = "read mismatch"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test list
|
||||||
|
if success then
|
||||||
|
local files = fs.list("/data")
|
||||||
|
if files then
|
||||||
|
log("Files in /data: " .. #files)
|
||||||
|
for _, f in ipairs(files) do
|
||||||
|
log(" - " .. f)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test delete
|
||||||
|
if success then
|
||||||
|
local deleteOk = fs.delete("/data/test.txt")
|
||||||
|
if deleteOk then
|
||||||
|
log("Delete successful")
|
||||||
|
else
|
||||||
|
success = false
|
||||||
|
msg = "delete failed"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if success then
|
||||||
|
setStatus("storage-status", "Passed", true)
|
||||||
|
log("Storage test complete!")
|
||||||
|
else
|
||||||
|
setStatus("storage-status", "Failed: " .. msg, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Initialize
|
||||||
|
log("Sandbox Test App loaded")
|
||||||
|
log("Lua version: " .. (_VERSION or "unknown"))
|
||||||
BIN
test-apps/com.mosis.sandbox-test/icon.tga
LFS
Normal file
BIN
test-apps/com.mosis.sandbox-test/icon.tga
LFS
Normal file
Binary file not shown.
46
test-apps/com.mosis.sandbox-test/main.rml
Normal file
46
test-apps/com.mosis.sandbox-test/main.rml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<rml>
|
||||||
|
<head>
|
||||||
|
<title>Sandbox Test</title>
|
||||||
|
<link type="text/rcss" href="styles.rcss"/>
|
||||||
|
<script src="app.lua"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-bar">
|
||||||
|
<div class="app-bar-nav btn-icon" onclick="goBack()">
|
||||||
|
<span class="icon">←</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-bar-title">Sandbox Test</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Timer Test</div>
|
||||||
|
<div id="timer-status">Not started</div>
|
||||||
|
<button onclick="testTimer()">Start Timer</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">JSON Test</div>
|
||||||
|
<div id="json-status">Not tested</div>
|
||||||
|
<button onclick="testJSON()">Test JSON</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Crypto Test</div>
|
||||||
|
<div id="crypto-status">Not tested</div>
|
||||||
|
<button onclick="testCrypto()">Test Crypto</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Storage Test</div>
|
||||||
|
<div id="storage-status">Not tested</div>
|
||||||
|
<button onclick="testStorage()">Test Storage</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Results</div>
|
||||||
|
<div id="results">Click buttons above to run tests</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</rml>
|
||||||
18
test-apps/com.mosis.sandbox-test/manifest.json
Normal file
18
test-apps/com.mosis.sandbox-test/manifest.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"id": "com.mosis.sandbox-test",
|
||||||
|
"name": "Sandbox Test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"version_code": 1,
|
||||||
|
"entry": "main.rml",
|
||||||
|
"icon": "icon.tga",
|
||||||
|
"description": "Tests sandbox APIs: timers, storage, JSON, crypto",
|
||||||
|
"developer": {
|
||||||
|
"name": "Mosis Team",
|
||||||
|
"email": "dev@mosis.dev"
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"network"
|
||||||
|
],
|
||||||
|
"min_api_version": 1
|
||||||
|
}
|
||||||
97
test-apps/com.mosis.sandbox-test/styles.rcss
Normal file
97
test-apps/com.mosis.sandbox-test/styles.rcss
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
body {
|
||||||
|
font-family: LatoLatin;
|
||||||
|
font-size: 16dp;
|
||||||
|
background-color: #121212;
|
||||||
|
color: #ffffff;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
height: 56dp;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
padding: 0 8dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bar-nav {
|
||||||
|
width: 40dp;
|
||||||
|
height: 40dp;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 20dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bar-nav:hover {
|
||||||
|
background-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 24dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bar-title {
|
||||||
|
font-size: 20dp;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 16dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: block;
|
||||||
|
padding: 16dp;
|
||||||
|
width: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: block;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
border-radius: 12dp;
|
||||||
|
padding: 16dp;
|
||||||
|
margin-bottom: 12dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 18dp;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8dp;
|
||||||
|
color: #bb86fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card div {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: block;
|
||||||
|
background-color: #bb86fc;
|
||||||
|
color: #000000;
|
||||||
|
border-width: 0;
|
||||||
|
border-radius: 8dp;
|
||||||
|
padding: 12dp 24dp;
|
||||||
|
font-size: 14dp;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 8dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #cf9fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background-color: #9a67ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
#results {
|
||||||
|
font-family: LatoLatin;
|
||||||
|
font-size: 12dp;
|
||||||
|
background-color: #0d0d0d;
|
||||||
|
padding: 12dp;
|
||||||
|
border-radius: 8dp;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: #00ff00;
|
||||||
|
}
|
||||||
21
test-apps/package.bat
Normal file
21
test-apps/package.bat
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
@echo off
|
||||||
|
REM Package test apps as .mosis files
|
||||||
|
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
for /d %%d in (*) do (
|
||||||
|
if exist "%%d\manifest.json" (
|
||||||
|
echo Packaging %%d...
|
||||||
|
cd %%d
|
||||||
|
if exist "..\%%d.mosis" del "..\%%d.mosis"
|
||||||
|
tar -a -cf "..\%%d.mosis" *
|
||||||
|
cd ..
|
||||||
|
echo Created %%d.mosis
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Done! Package files:
|
||||||
|
dir /b *.mosis 2>nul
|
||||||
|
|
||||||
|
endlocal
|
||||||
Reference in New Issue
Block a user