13 KiB
13 KiB
Milestone 11: Virtual Hardware - Camera
Status: Complete Goal: Camera access with mandatory recording indicators and security controls.
Overview
This milestone implements secure camera access for Lua apps:
- Permission required (
camerapermission) - User gesture required to start session
- Mandatory recording indicator (system-controlled, cannot be hidden)
- Single session per app
- Frame rate limiting
- Automatic cleanup on app stop
Key Deliverables
- CameraInterface class - Session management, permission checks
- CameraSession class - Active camera session wrapper
- Lua camera API -
camera.start(), session methods - Recording indicator - System-level UI notification
File Structure
src/main/cpp/sandbox/
├── camera_interface.h # NEW - Camera API header
└── camera_interface.cpp # NEW - Camera implementation
Implementation Details
1. CameraInterface Class
// camera_interface.h
#pragma once
#include <string>
#include <memory>
#include <functional>
#include <mutex>
#include <atomic>
#include <vector>
struct lua_State;
namespace mosis {
class PermissionGate;
enum class CameraFacing {
Front,
Back
};
enum class CameraResolution {
VGA, // 640x480
HD, // 1280x720
FullHD // 1920x1080
};
struct CameraConfig {
CameraFacing facing = CameraFacing::Back;
CameraResolution resolution = CameraResolution::HD;
int max_fps = 30;
};
struct CameraFrame {
std::vector<uint8_t> data; // RGBA or JPEG data
int width;
int height;
uint64_t timestamp_ms;
bool is_jpeg;
};
class CameraSession {
public:
using FrameCallback = std::function<void(const CameraFrame& frame)>;
CameraSession(int id, const CameraConfig& config);
~CameraSession();
int GetId() const { return m_id; }
const CameraConfig& GetConfig() const { return m_config; }
bool IsActive() const { return m_active; }
// Capture single photo (returns copy of current frame)
CameraFrame Capture();
// Set frame callback (for preview)
void SetOnFrame(FrameCallback cb) { m_on_frame = std::move(cb); }
// Stop session
void Stop();
// For mock mode - simulate frame arrival
void SimulateFrame(const CameraFrame& frame);
private:
int m_id;
CameraConfig m_config;
std::atomic<bool> m_active{true};
FrameCallback m_on_frame;
CameraFrame m_last_frame;
std::mutex m_mutex;
};
class CameraInterface {
public:
CameraInterface(const std::string& app_id, PermissionGate* permissions);
~CameraInterface();
// Start camera session
// Returns session on success, nullptr on failure (sets error)
// Requires camera permission and user gesture
std::shared_ptr<CameraSession> StartSession(const CameraConfig& config, std::string& error);
// Stop active session
void StopSession();
// Check if session is active
bool HasActiveSession() const;
// Check if recording indicator should be shown
bool IsIndicatorVisible() const;
// Cleanup on app stop
void Shutdown();
// For testing
void SetMockMode(bool enabled) { m_mock_mode = enabled; }
bool IsMockMode() const { return m_mock_mode; }
// Simulate user gesture for testing
void SimulateUserGesture();
private:
std::string m_app_id;
PermissionGate* m_permissions;
std::shared_ptr<CameraSession> m_active_session;
mutable std::mutex m_mutex;
bool m_mock_mode = true;
std::atomic<bool> m_indicator_visible{false};
// Track user gesture timing
uint64_t m_last_gesture_time = 0;
static constexpr uint64_t GESTURE_VALIDITY_MS = 5000; // 5 seconds
bool HasRecentUserGesture() const;
};
// Register camera.* APIs as globals
void RegisterCameraAPI(lua_State* L, CameraInterface* camera);
} // namespace mosis
2. Permission Requirements
Camera access requires:
camerapermission declared in manifest- Permission granted by user (dangerous permission)
- Recent user gesture (within 5 seconds)
// Permission check flow
bool CameraInterface::StartSession(...) {
// 1. Check permission
if (!m_permissions->Check("camera")) {
error = "Camera permission not granted";
return nullptr;
}
// 2. Check user gesture
if (!HasRecentUserGesture()) {
error = "Camera requires user gesture";
return nullptr;
}
// 3. Check no existing session
if (m_active_session) {
error = "Camera session already active";
return nullptr;
}
// ... create session
}
3. Recording Indicator
The recording indicator is mandatory and system-controlled:
- Shown whenever camera session is active
- Cannot be hidden or obscured by app
- Positioned in system UI area
- Shows camera icon with "Recording" text
// Indicator control
void CameraInterface::ShowIndicator() {
m_indicator_visible = true;
// In real implementation: notify system UI
}
void CameraInterface::HideIndicator() {
m_indicator_visible = false;
// In real implementation: notify system UI
}
4. Lua API
-- Start camera session (requires permission + user gesture)
local session, err = camera.start({
facing = "back", -- "front" or "back"
resolution = "720p", -- "480p", "720p", or "1080p"
fps = 30 -- max frames per second
})
if not session then
print("Failed to start camera:", err)
return
end
-- Capture single photo
local photo = session:capture()
if photo then
print("Captured", photo.width, "x", photo.height, "image")
-- photo.data contains JPEG bytes
end
-- Set frame callback (for preview)
session:on("frame", function(frame)
-- frame.data, frame.width, frame.height, frame.timestamp
end)
-- Stop session
session:stop()
-- Check if camera is active
if camera.isActive() then
print("Camera is running")
end
5. Frame Rate Limiting
To prevent resource abuse:
- Maximum 30 FPS
- Minimum 16ms between frames
- Frame delivery is asynchronous
struct FrameRateLimiter {
int max_fps = 30;
uint64_t min_frame_interval_ms = 33; // ~30 FPS
uint64_t last_frame_time = 0;
bool ShouldDeliverFrame() {
uint64_t now = GetCurrentTimeMs();
if (now - last_frame_time >= min_frame_interval_ms) {
last_frame_time = now;
return true;
}
return false;
}
};
6. Session Lifecycle
User Gesture ──► camera.start() ──► Session Active ──► Indicator Shown
│ │
│ error │ session:stop()
▼ ▼
nil, err Session Closed ──► Indicator Hidden
App Stop ──► CameraInterface::Shutdown() ──► All Sessions Closed
Test Cases
Test 1: Requires Permission
bool Test_CameraRequiresPermission(std::string& error_msg) {
// Create permission gate WITHOUT camera permission
PermissionGate permissions("test.app", {}, false);
mosis::CameraInterface camera("test.app", &permissions);
camera.SimulateUserGesture();
std::string err;
mosis::CameraConfig config;
auto session = camera.StartSession(config, err);
EXPECT_TRUE(session == nullptr);
EXPECT_TRUE(err.find("permission") != std::string::npos);
return true;
}
Test 2: Requires User Gesture
bool Test_CameraRequiresUserGesture(std::string& error_msg) {
// Create permission gate WITH camera permission
PermissionGate permissions("test.app", {"camera"}, false);
permissions.Grant("camera");
mosis::CameraInterface camera("test.app", &permissions);
// Note: NOT calling SimulateUserGesture()
std::string err;
mosis::CameraConfig config;
auto session = camera.StartSession(config, err);
EXPECT_TRUE(session == nullptr);
EXPECT_TRUE(err.find("gesture") != std::string::npos);
return true;
}
Test 3: Shows Indicator
bool Test_CameraShowsIndicator(std::string& error_msg) {
PermissionGate permissions("test.app", {"camera"}, false);
permissions.Grant("camera");
mosis::CameraInterface camera("test.app", &permissions);
camera.SimulateUserGesture();
EXPECT_FALSE(camera.IsIndicatorVisible());
std::string err;
mosis::CameraConfig config;
auto session = camera.StartSession(config, err);
// Indicator should be visible while session is active
EXPECT_TRUE(camera.IsIndicatorVisible());
session->Stop();
// Indicator should be hidden after stop
EXPECT_FALSE(camera.IsIndicatorVisible());
return true;
}
Test 4: Single Session Only
bool Test_CameraSingleSession(std::string& error_msg) {
PermissionGate permissions("test.app", {"camera"}, false);
permissions.Grant("camera");
mosis::CameraInterface camera("test.app", &permissions);
camera.SimulateUserGesture();
std::string err;
mosis::CameraConfig config;
// First session should succeed
auto session1 = camera.StartSession(config, err);
EXPECT_TRUE(session1 != nullptr);
// Second session should fail
camera.SimulateUserGesture();
auto session2 = camera.StartSession(config, err);
EXPECT_TRUE(session2 == nullptr);
EXPECT_TRUE(err.find("active") != std::string::npos ||
err.find("already") != std::string::npos);
return true;
}
Test 5: Stops On App Stop
bool Test_CameraStopsOnAppStop(std::string& error_msg) {
PermissionGate permissions("test.app", {"camera"}, false);
permissions.Grant("camera");
mosis::CameraInterface camera("test.app", &permissions);
camera.SimulateUserGesture();
std::string err;
mosis::CameraConfig config;
auto session = camera.StartSession(config, err);
EXPECT_TRUE(session != nullptr);
EXPECT_TRUE(camera.HasActiveSession());
// Simulate app stop
camera.Shutdown();
EXPECT_FALSE(camera.HasActiveSession());
EXPECT_FALSE(camera.IsIndicatorVisible());
return true;
}
Test 6: Lua Integration
bool Test_CameraLuaIntegration(std::string& error_msg) {
SandboxContext ctx = TestContext();
ctx.permissions = {"camera"};
LuaSandbox sandbox(ctx);
PermissionGate permissions("test.app", {"camera"}, false);
permissions.Grant("camera");
mosis::CameraInterface camera("test.app", &permissions);
camera.SimulateUserGesture();
mosis::RegisterCameraAPI(sandbox.GetState(), &camera);
std::string script = R"lua(
-- Test that camera global exists
if not camera then
error("camera global not found")
end
if not camera.start then
error("camera.start not found")
end
if not camera.isActive then
error("camera.isActive not found")
end
-- isActive should be false initially
if camera.isActive() then
error("camera should not be active initially")
end
)lua";
bool ok = sandbox.LoadString(script, "camera_test");
if (!ok) {
error_msg = "Lua test failed: " + sandbox.GetLastError();
return false;
}
return true;
}
Acceptance Criteria
All tests must pass:
Test_CameraRequiresPermission- Permission check worksTest_CameraRequiresUserGesture- User gesture requiredTest_CameraShowsIndicator- Recording indicator shown/hiddenTest_CameraSingleSession- Only one session allowedTest_CameraStopsOnShutdown- Cleanup on shutdownTest_CameraLuaIntegration- Lua API works
Dependencies
- Milestone 1 (LuaSandbox)
- Milestone 2 (PermissionGate + user gesture)
- Milestone 3 (AuditLog)
Notes
Desktop vs Android Implementation
For desktop testing, CameraInterface operates in mock mode:
- Permission and gesture checks run normally
- Indicator state is tracked but not displayed
- No actual camera hardware access
- Frames can be simulated for testing
On Android, the real implementation would:
- Use Camera2 API through JNI
- Display system-level recording indicator
- Handle camera hardware lifecycle
- Deliver frames through SurfaceTexture
Security Considerations
- Permission: Camera access is a dangerous permission requiring user grant
- User gesture: Prevents background camera activation
- Indicator: User always knows when camera is active
- Single session: Prevents resource abuse
- Cleanup: Sessions closed when app stops
Future Integration
The camera will integrate with game engines to:
- Stream frames to VR/AR display
- Support mixed reality passthrough
- Enable barcode/QR scanning
Next Steps
After Milestone 11 passes:
- Milestone 12: Virtual Hardware - Microphone