# Milestone 12: Virtual Hardware - Microphone **Status**: Complete **Goal**: Audio recording with mandatory recording indicators and security controls. --- ## Overview This milestone implements secure microphone access for Lua apps: - Permission required (`microphone` permission) - User gesture required to start recording - Mandatory recording indicator (system-controlled, cannot be hidden) - Single recording session per app - Sample rate limiting - Automatic cleanup on app stop ### Key Deliverables 1. **MicrophoneInterface class** - Session management, permission checks 2. **RecordingSession class** - Active recording session wrapper 3. **Lua microphone API** - `microphone.start()`, session methods 4. **Recording indicator** - System-level UI notification --- ## File Structure ``` src/main/cpp/sandbox/ ├── microphone_interface.h # NEW - Microphone API header └── microphone_interface.cpp # NEW - Microphone implementation ``` --- ## Implementation Details ### 1. MicrophoneInterface Class ```cpp // microphone_interface.h #pragma once #include #include #include #include #include #include #include struct lua_State; namespace mosis { class PermissionGate; enum class AudioFormat { PCM_16BIT, PCM_FLOAT }; struct RecordingConfig { int sample_rate = 44100; // 8000, 16000, 22050, 44100, 48000 int channels = 1; // 1 (mono) or 2 (stereo) AudioFormat format = AudioFormat::PCM_16BIT; }; struct AudioBuffer { std::vector data; int sample_rate; int channels; AudioFormat format; uint64_t timestamp_ms; int sample_count; }; class RecordingSession { public: using BufferCallback = std::function; RecordingSession(int id, const RecordingConfig& config); ~RecordingSession(); int GetId() const { return m_id; } const RecordingConfig& GetConfig() const { return m_config; } bool IsActive() const { return m_active; } // Get accumulated audio data AudioBuffer GetBuffer(); // Set buffer callback (for streaming) void SetOnBuffer(BufferCallback cb) { m_on_buffer = std::move(cb); } // Stop recording void Stop(); // For mock mode - simulate audio data arrival void SimulateBuffer(const AudioBuffer& buffer); private: int m_id; RecordingConfig m_config; std::atomic m_active{true}; BufferCallback m_on_buffer; AudioBuffer m_accumulated; mutable std::mutex m_mutex; }; class MicrophoneInterface { public: MicrophoneInterface(const std::string& app_id, PermissionGate* permissions); ~MicrophoneInterface(); // Start recording session // Returns session on success, nullptr on failure (sets error) // Requires microphone permission and user gesture std::shared_ptr StartSession(const RecordingConfig& 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 m_active_session; mutable std::mutex m_mutex; bool m_mock_mode = true; std::atomic m_indicator_visible{false}; int m_next_session_id = 1; // Track user gesture timing std::chrono::steady_clock::time_point m_last_gesture_time; bool m_has_gesture = false; static constexpr int GESTURE_VALIDITY_MS = 5000; // 5 seconds bool HasRecentUserGesture() const; void ShowIndicator(); void HideIndicator(); }; // Register microphone.* APIs as globals void RegisterMicrophoneAPI(lua_State* L, MicrophoneInterface* microphone); } // namespace mosis ``` ### 2. Permission Requirements Microphone access requires: 1. `microphone` permission declared in manifest 2. Permission granted by user (dangerous permission) 3. Recent user gesture (within 5 seconds) ```cpp // Permission check flow bool MicrophoneInterface::StartSession(...) { // 1. Check permission if (!m_permissions->HasPermission("microphone")) { error = "Microphone permission not granted"; return nullptr; } // 2. Check user gesture if (!HasRecentUserGesture()) { error = "Microphone requires user gesture"; return nullptr; } // 3. Check no existing session if (m_active_session) { error = "Recording session already active"; return nullptr; } // ... create session } ``` ### 3. Recording Indicator The recording indicator is mandatory and system-controlled: - Shown whenever microphone session is active - Cannot be hidden or obscured by app - Positioned in system UI area - Shows microphone icon with "Recording" text ### 4. Lua API ```lua -- Start recording session (requires permission + user gesture) local session, err = microphone.start({ sampleRate = 44100, -- 8000, 16000, 22050, 44100, 48000 channels = 1, -- 1 (mono) or 2 (stereo) format = "pcm16" -- "pcm16" or "float" }) if not session then print("Failed to start recording:", err) return end -- Set buffer callback (for streaming audio) session:on("buffer", function(buffer) -- buffer.data, buffer.sampleRate, buffer.channels, buffer.sampleCount end) -- Get accumulated audio data local audio = session:getBuffer() if audio then print("Recorded", audio.sampleCount, "samples at", audio.sampleRate, "Hz") end -- Stop recording session:stop() -- Check if microphone is active if microphone.isActive() then print("Microphone is recording") end ``` ### 5. Sample Rate Validation To prevent resource abuse: - Allowed sample rates: 8000, 16000, 22050, 44100, 48000 - Maximum: 48000 Hz - Channels: 1 (mono) or 2 (stereo) ### 6. Session Lifecycle ``` User Gesture ──► microphone.start() ──► Session Active ──► Indicator Shown │ │ │ error │ session:stop() ▼ ▼ nil, err Session Closed ──► Indicator Hidden App Stop ──► MicrophoneInterface::Shutdown() ──► All Sessions Closed ``` --- ## Test Cases ### Test 1: Requires Permission ```cpp bool Test_MicrophoneRequiresPermission(std::string& error_msg) { // Create context WITHOUT microphone permission SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {}; ctx.is_system_app = false; PermissionGate permissions(ctx); mosis::MicrophoneInterface mic("test.app", &permissions); mic.SimulateUserGesture(); std::string err; mosis::RecordingConfig config; auto session = mic.StartSession(config, err); EXPECT_TRUE(session == nullptr); EXPECT_TRUE(err.find("permission") != std::string::npos); return true; } ``` ### Test 2: Requires User Gesture ```cpp bool Test_MicrophoneRequiresUserGesture(std::string& error_msg) { SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {"microphone"}; ctx.is_system_app = false; PermissionGate permissions(ctx); permissions.GrantPermission("microphone"); mosis::MicrophoneInterface mic("test.app", &permissions); // Note: NOT calling SimulateUserGesture() std::string err; mosis::RecordingConfig config; auto session = mic.StartSession(config, err); EXPECT_TRUE(session == nullptr); EXPECT_TRUE(err.find("gesture") != std::string::npos); return true; } ``` ### Test 3: Shows Indicator ```cpp bool Test_MicrophoneShowsIndicator(std::string& error_msg) { SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {"microphone"}; ctx.is_system_app = false; PermissionGate permissions(ctx); permissions.GrantPermission("microphone"); mosis::MicrophoneInterface mic("test.app", &permissions); mic.SimulateUserGesture(); EXPECT_FALSE(mic.IsIndicatorVisible()); std::string err; mosis::RecordingConfig config; auto session = mic.StartSession(config, err); EXPECT_TRUE(mic.IsIndicatorVisible()); mic.StopSession(); EXPECT_FALSE(mic.IsIndicatorVisible()); return true; } ``` ### Test 4: Single Session Only ```cpp bool Test_MicrophoneSingleSession(std::string& error_msg) { SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {"microphone"}; ctx.is_system_app = false; PermissionGate permissions(ctx); permissions.GrantPermission("microphone"); mosis::MicrophoneInterface mic("test.app", &permissions); mic.SimulateUserGesture(); std::string err; mosis::RecordingConfig config; // First session should succeed auto session1 = mic.StartSession(config, err); EXPECT_TRUE(session1 != nullptr); // Second session should fail mic.SimulateUserGesture(); auto session2 = mic.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 ```cpp bool Test_MicrophoneStopsOnShutdown(std::string& error_msg) { SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {"microphone"}; ctx.is_system_app = false; PermissionGate permissions(ctx); permissions.GrantPermission("microphone"); mosis::MicrophoneInterface mic("test.app", &permissions); mic.SimulateUserGesture(); std::string err; mosis::RecordingConfig config; auto session = mic.StartSession(config, err); EXPECT_TRUE(session != nullptr); EXPECT_TRUE(mic.HasActiveSession()); // Simulate app stop mic.Shutdown(); EXPECT_FALSE(mic.HasActiveSession()); EXPECT_FALSE(mic.IsIndicatorVisible()); return true; } ``` ### Test 6: Lua Integration ```cpp bool Test_MicrophoneLuaIntegration(std::string& error_msg) { SandboxContext ctx = TestContext(); ctx.permissions = {"microphone"}; LuaSandbox sandbox(ctx); PermissionGate permissions(ctx); permissions.GrantPermission("microphone"); mosis::MicrophoneInterface mic("test.app", &permissions); mic.SimulateUserGesture(); mosis::RegisterMicrophoneAPI(sandbox.GetState(), &mic); std::string script = R"lua( -- Test that microphone global exists if not microphone then error("microphone global not found") end if not microphone.start then error("microphone.start not found") end if not microphone.isActive then error("microphone.isActive not found") end -- isActive should be false initially if microphone.isActive() then error("microphone should not be active initially") end )lua"; bool ok = sandbox.LoadString(script, "microphone_test"); if (!ok) { error_msg = "Lua test failed: " + sandbox.GetLastError(); return false; } return true; } ``` --- ## Acceptance Criteria All tests must pass: - [x] `Test_MicrophoneRequiresPermission` - Permission check works - [x] `Test_MicrophoneRequiresUserGesture` - User gesture required - [x] `Test_MicrophoneShowsIndicator` - Recording indicator shown/hidden - [x] `Test_MicrophoneSingleSession` - Only one session allowed - [x] `Test_MicrophoneStopsOnShutdown` - Cleanup on shutdown - [x] `Test_MicrophoneLuaIntegration` - Lua API works --- ## Dependencies - Milestone 1 (LuaSandbox) - Milestone 2 (PermissionGate + user gesture) - Milestone 3 (AuditLog) --- ## Notes ### Desktop vs Android Implementation For desktop testing, MicrophoneInterface operates in mock mode: - Permission and gesture checks run normally - Indicator state is tracked but not displayed - No actual microphone hardware access - Audio buffers can be simulated for testing On Android, the real implementation would: 1. Use AudioRecord API through JNI 2. Display system-level recording indicator 3. Handle audio hardware lifecycle 4. Deliver audio buffers through native callbacks ### Security Considerations 1. **Permission**: Microphone access is a dangerous permission requiring user grant 2. **User gesture**: Prevents background audio recording 3. **Indicator**: User always knows when microphone is active 4. **Single session**: Prevents resource abuse 5. **Cleanup**: Sessions closed when app stops ### Future Integration The microphone will integrate with: 1. Voice communication in multiplayer games 2. Voice commands/speech recognition 3. Audio messaging features --- ## Next Steps After Milestone 12 passes: 1. Milestone 13: Virtual Hardware - Audio Output