# Milestone 16: Virtual Hardware - Bluetooth **Status**: Complete **Goal**: Bluetooth discovery and pairing with user consent. --- ## Overview This milestone implements secure Bluetooth access for Lua apps: - Discovery requires user consent (user gesture) - Permission required: `bluetooth` for discovery and connection - Device info limited to name and address (no UUIDs) - Connection limit: 5 active connections per app - Automatic cleanup on app stop ### Key Deliverables 1. **BluetoothInterface class** - Bluetooth discovery and connection management 2. **BluetoothConnection class** - Individual connection handling 3. **Lua bluetooth API** - `bluetooth.startDiscovery()`, `bluetooth.connect()` 4. **Privacy protection** - Limited device information, user consent required --- ## File Structure ``` src/main/cpp/sandbox/ ├── bluetooth_interface.h # NEW - Bluetooth API header └── bluetooth_interface.cpp # NEW - Bluetooth implementation ``` --- ## Implementation Details ### 1. BluetoothInterface Class ```cpp // bluetooth_interface.h #pragma once #include #include #include #include #include #include #include #include struct lua_State; namespace mosis { struct BluetoothDevice { std::string name; // Device display name std::string address; // MAC address }; struct DiscoveryOptions { int timeout_seconds = 30; // Discovery timeout }; struct ConnectionOptions { int timeout_ms = 10000; // Connection timeout }; enum class BluetoothError { None, PermissionDenied, UserConsentRequired, DiscoveryFailed, ConnectionFailed, NotConnected, ConnectionLimitReached, Timeout, AlreadyDiscovering }; class BluetoothConnection { public: using DataCallback = std::function&)>; using ErrorCallback = std::function; BluetoothConnection(int id, const std::string& address); ~BluetoothConnection(); int GetId() const { return m_id; } std::string GetAddress() const { return m_address; } bool IsConnected() const { return m_connected; } bool Send(const std::vector& data, std::string& error); void Close(); void SetOnData(DataCallback cb) { m_on_data = std::move(cb); } void SetOnError(ErrorCallback cb) { m_on_error = std::move(cb); } void SetOnDisconnect(std::function cb) { m_on_disconnect = std::move(cb); } // For mock mode - simulate data receive void SimulateData(const std::vector& data); void SimulateDisconnect(); private: int m_id; std::string m_address; std::atomic m_connected{false}; DataCallback m_on_data; ErrorCallback m_on_error; std::function m_on_disconnect; mutable std::mutex m_mutex; }; class BluetoothInterface { public: BluetoothInterface(const std::string& app_id); ~BluetoothInterface(); // Permission checks bool HasBluetoothPermission() const { return m_has_bluetooth_permission; } void SetBluetoothPermission(bool granted) { m_has_bluetooth_permission = granted; } // User gesture tracking bool HasUserConsent() const { return m_has_user_consent; } void SetUserConsent(bool consent) { m_has_user_consent = consent; } // Start device discovery // Requires permission AND user consent bool StartDiscovery( const DiscoveryOptions& options, std::function&)> success, std::function error, std::string& out_error ); // Stop current discovery void StopDiscovery(); // Check if discovery is in progress bool IsDiscovering() const { return m_is_discovering; } // Connect to a device std::shared_ptr Connect( const std::string& address, const ConnectionOptions& options, std::string& error ); // Disconnect a specific connection void Disconnect(int connection_id); // Disconnect all connections void DisconnectAll(); // Get active connection count size_t GetActiveConnectionCount() const; // Cleanup on app stop void Shutdown(); // For testing void SetMockMode(bool enabled) { m_mock_mode = enabled; } bool IsMockMode() const { return m_mock_mode; } // Mock mode: set discovered devices void SetMockDevices(const std::vector& devices) { m_mock_devices = devices; } // Get connection by ID std::shared_ptr GetConnection(int id); private: std::string m_app_id; std::unordered_map> m_connections; mutable std::mutex m_mutex; bool m_mock_mode = true; bool m_has_bluetooth_permission = false; bool m_has_user_consent = false; std::atomic m_is_discovering{false}; std::vector m_mock_devices; int m_next_connection_id = 1; static constexpr int MAX_CONNECTIONS = 5; void CleanupClosedConnections(); }; // Register bluetooth.* APIs as globals void RegisterBluetoothAPI(lua_State* L, BluetoothInterface* bluetooth); } // namespace mosis ``` ### 2. Permission and Consent Requirements | Action | Permission Required | User Consent Required | |--------|--------------------|-----------------------| | Start Discovery | `bluetooth` | Yes (user gesture) | | Connect to Device | `bluetooth` | No (implicit from discovery) | | Send Data | `bluetooth` | No (connection already established) | | Receive Data | `bluetooth` | No (connection already established) | ### 3. Privacy Protection Device information is limited to: - Device name (display name) - Device address (MAC) NOT exposed: - Full UUID list (prevents service fingerprinting) - Device class - Manufacturer data - RSSI (prevents proximity tracking) ### 4. Lua API ```lua -- Start device discovery (requires user gesture) bluetooth.startDiscovery(function(devices) for _, device in ipairs(devices) do print("Found:", device.name, device.address) end end, function(error, message) print("Discovery error:", error, message) end, { timeout = 30 -- seconds }) -- Stop discovery bluetooth.stopDiscovery() -- Check if discovering local discovering = bluetooth.isDiscovering() -- Connect to a device local conn = bluetooth.connect(address, function(data) print("Received:", #data, "bytes") end, function(error, message) print("Connection error:", error, message) end, { timeout = 10000 -- ms }) -- Check connection if conn and conn:isConnected() then -- Send data conn:send({0x01, 0x02, 0x03}) -- Get connection info print("Connected to:", conn:getAddress()) print("Connection ID:", conn:getId()) end -- Close connection conn:close() -- Get active connection count local count = bluetooth.getConnectionCount() -- Disconnect all bluetooth.disconnectAll() ``` ### 5. Error Codes ```lua -- Error values passed to error callbacks "PERMISSION_DENIED" -- No bluetooth permission "USER_CONSENT_REQUIRED" -- User gesture required for discovery "DISCOVERY_FAILED" -- Discovery could not start "CONNECTION_FAILED" -- Could not connect to device "NOT_CONNECTED" -- Connection not established "CONNECTION_LIMIT" -- Max connections reached (5) "TIMEOUT" -- Operation timed out "ALREADY_DISCOVERING" -- Discovery already in progress ``` --- ## Test Cases ### Test 1: Requires Permission ```cpp bool Test_BluetoothRequiresPermission(std::string& error_msg) { mosis::BluetoothInterface bluetooth("test.app"); // No permission granted std::string err; bool started = bluetooth.StartDiscovery( {}, [](const std::vector&) {}, [](mosis::BluetoothError, const std::string&) {}, err ); EXPECT_TRUE(!started); EXPECT_TRUE(err.find("permission") != std::string::npos); return true; } ``` ### Test 2: Requires User Consent ```cpp bool Test_BluetoothRequiresUserConsent(std::string& error_msg) { mosis::BluetoothInterface bluetooth("test.app"); bluetooth.SetBluetoothPermission(true); // User consent NOT granted std::string err; bool started = bluetooth.StartDiscovery( {}, [](const std::vector&) {}, [](mosis::BluetoothError, const std::string&) {}, err ); EXPECT_TRUE(!started); EXPECT_TRUE(err.find("consent") != std::string::npos || err.find("gesture") != std::string::npos); return true; } ``` ### Test 3: Discovery Works With Permission and Consent ```cpp bool Test_BluetoothDiscoveryWorks(std::string& error_msg) { mosis::BluetoothInterface bluetooth("test.app"); bluetooth.SetBluetoothPermission(true); bluetooth.SetUserConsent(true); // Set mock devices std::vector devices = { {"Device A", "AA:BB:CC:DD:EE:FF"}, {"Device B", "11:22:33:44:55:66"} }; bluetooth.SetMockDevices(devices); std::vector discovered; bool got_devices = false; std::string err; bool started = bluetooth.StartDiscovery( {}, [&](const std::vector& devs) { got_devices = true; discovered = devs; }, [](mosis::BluetoothError, const std::string&) {}, err ); EXPECT_TRUE(started); EXPECT_TRUE(got_devices); EXPECT_TRUE(discovered.size() == 2); EXPECT_TRUE(discovered[0].name == "Device A"); EXPECT_TRUE(discovered[0].address == "AA:BB:CC:DD:EE:FF"); return true; } ``` ### Test 4: Connection Limit ```cpp bool Test_BluetoothConnectionLimit(std::string& error_msg) { mosis::BluetoothInterface bluetooth("test.app"); bluetooth.SetBluetoothPermission(true); std::vector> connections; std::string err; // Create MAX_CONNECTIONS (5) for (int i = 0; i < 5; i++) { char addr[20]; snprintf(addr, sizeof(addr), "AA:BB:CC:DD:EE:%02X", i); auto conn = bluetooth.Connect(addr, {}, err); EXPECT_TRUE(conn != nullptr); connections.push_back(conn); } // 6th should fail auto extra = bluetooth.Connect("AA:BB:CC:DD:EE:FF", {}, err); EXPECT_TRUE(extra == nullptr); EXPECT_TRUE(err.find("limit") != std::string::npos); return true; } ``` ### Test 5: Connection Send/Receive ```cpp bool Test_BluetoothSendReceive(std::string& error_msg) { mosis::BluetoothInterface bluetooth("test.app"); bluetooth.SetBluetoothPermission(true); std::string err; auto conn = bluetooth.Connect("AA:BB:CC:DD:EE:FF", {}, err); EXPECT_TRUE(conn != nullptr); EXPECT_TRUE(conn->IsConnected()); // Test send std::vector data = {0x01, 0x02, 0x03}; bool sent = conn->Send(data, err); EXPECT_TRUE(sent); // Test receive via simulation std::vector received; bool got_data = false; conn->SetOnData([&](const std::vector& d) { got_data = true; received = d; }); std::vector incoming = {0xAA, 0xBB}; conn->SimulateData(incoming); EXPECT_TRUE(got_data); EXPECT_TRUE(received.size() == 2); EXPECT_TRUE(received[0] == 0xAA); return true; } ``` ### Test 6: Cleanup on Shutdown ```cpp bool Test_BluetoothCleansUpOnShutdown(std::string& error_msg) { mosis::BluetoothInterface bluetooth("test.app"); bluetooth.SetBluetoothPermission(true); std::string err; auto conn1 = bluetooth.Connect("AA:BB:CC:DD:EE:01", {}, err); auto conn2 = bluetooth.Connect("AA:BB:CC:DD:EE:02", {}, err); EXPECT_TRUE(bluetooth.GetActiveConnectionCount() == 2); bluetooth.Shutdown(); EXPECT_TRUE(bluetooth.GetActiveConnectionCount() == 0); EXPECT_TRUE(!conn1->IsConnected()); EXPECT_TRUE(!conn2->IsConnected()); return true; } ``` ### Test 7: Lua Integration ```cpp bool Test_BluetoothLuaIntegration(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::BluetoothInterface bluetooth("test.app"); mosis::RegisterBluetoothAPI(sandbox.GetState(), &bluetooth); std::string script = R"lua( -- Test that bluetooth global exists if not bluetooth then error("bluetooth global not found") end if not bluetooth.startDiscovery then error("bluetooth.startDiscovery not found") end if not bluetooth.stopDiscovery then error("bluetooth.stopDiscovery not found") end if not bluetooth.connect then error("bluetooth.connect not found") end if not bluetooth.disconnectAll then error("bluetooth.disconnectAll not found") end if not bluetooth.getConnectionCount then error("bluetooth.getConnectionCount not found") end if not bluetooth.isDiscovering then error("bluetooth.isDiscovering not found") end -- Connection count should be 0 initially if bluetooth.getConnectionCount() ~= 0 then error("should have no active connections initially") end -- Should not be discovering initially if bluetooth.isDiscovering() then error("should not be discovering initially") end )lua"; bool ok = sandbox.LoadString(script, "bluetooth_test"); if (!ok) { error_msg = "Lua test failed: " + sandbox.GetLastError(); return false; } return true; } ``` --- ## Acceptance Criteria All tests must pass: - [x] `Test_BluetoothRequiresPermission` - Permission required - [x] `Test_BluetoothRequiresUserConsent` - User consent required for discovery - [x] `Test_BluetoothDiscoveryWorks` - Discovery returns devices - [x] `Test_BluetoothConnectionLimit` - Max 5 connections enforced - [x] `Test_BluetoothSendReceive` - Send/receive data works - [x] `Test_BluetoothCleansUpOnShutdown` - Cleanup on shutdown - [x] `Test_BluetoothLuaIntegration` - Lua API works --- ## Dependencies - Milestone 1 (LuaSandbox) - Milestone 2 (PermissionGate + user gesture tracking) --- ## Notes ### Desktop vs Android Implementation For desktop testing, BluetoothInterface operates in mock mode: - Discovery returns mock device list - Connections track state but don't communicate - Send/receive can be simulated for testing On Android, the real implementation would: 1. Use BluetoothAdapter via JNI 2. Handle BLUETOOTH_SCAN and BLUETOOTH_CONNECT permissions 3. Use BluetoothSocket for RFCOMM connections 4. Support BLE via BluetoothGatt ### Security Considerations 1. **Permission required**: `bluetooth` permission needed for all operations 2. **User consent**: Discovery requires active user gesture 3. **Limited device info**: Only name and address exposed (no UUIDs) 4. **Connection limit**: Max 5 connections per app 5. **Cleanup**: All connections closed when app stops ### Privacy Features 1. **No service UUIDs**: Prevents detailed device fingerprinting 2. **No RSSI**: Prevents proximity tracking 3. **User consent**: Discovery requires explicit user action 4. **Audit logging**: All Bluetooth operations logged --- ## Next Steps After Milestone 16 passes: 1. Milestone 17: Virtual Hardware - Contacts