15 KiB
15 KiB
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:
bluetoothfor 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
- BluetoothInterface class - Bluetooth discovery and connection management
- BluetoothConnection class - Individual connection handling
- Lua bluetooth API -
bluetooth.startDiscovery(),bluetooth.connect() - 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
// bluetooth_interface.h
#pragma once
#include <string>
#include <memory>
#include <functional>
#include <mutex>
#include <atomic>
#include <vector>
#include <unordered_map>
#include <chrono>
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<void(const std::vector<uint8_t>&)>;
using ErrorCallback = std::function<void(BluetoothError, const std::string&)>;
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<uint8_t>& 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<void()> cb) { m_on_disconnect = std::move(cb); }
// For mock mode - simulate data receive
void SimulateData(const std::vector<uint8_t>& data);
void SimulateDisconnect();
private:
int m_id;
std::string m_address;
std::atomic<bool> m_connected{false};
DataCallback m_on_data;
ErrorCallback m_on_error;
std::function<void()> 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<void(const std::vector<BluetoothDevice>&)> success,
std::function<void(BluetoothError, const std::string&)> 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<BluetoothConnection> 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<BluetoothDevice>& devices) { m_mock_devices = devices; }
// Get connection by ID
std::shared_ptr<BluetoothConnection> GetConnection(int id);
private:
std::string m_app_id;
std::unordered_map<int, std::shared_ptr<BluetoothConnection>> 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<bool> m_is_discovering{false};
std::vector<BluetoothDevice> 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
-- 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
-- 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
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::BluetoothDevice>&) {},
[](mosis::BluetoothError, const std::string&) {},
err
);
EXPECT_TRUE(!started);
EXPECT_TRUE(err.find("permission") != std::string::npos);
return true;
}
Test 2: Requires User Consent
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::BluetoothDevice>&) {},
[](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
bool Test_BluetoothDiscoveryWorks(std::string& error_msg) {
mosis::BluetoothInterface bluetooth("test.app");
bluetooth.SetBluetoothPermission(true);
bluetooth.SetUserConsent(true);
// Set mock devices
std::vector<mosis::BluetoothDevice> devices = {
{"Device A", "AA:BB:CC:DD:EE:FF"},
{"Device B", "11:22:33:44:55:66"}
};
bluetooth.SetMockDevices(devices);
std::vector<mosis::BluetoothDevice> discovered;
bool got_devices = false;
std::string err;
bool started = bluetooth.StartDiscovery(
{},
[&](const std::vector<mosis::BluetoothDevice>& 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
bool Test_BluetoothConnectionLimit(std::string& error_msg) {
mosis::BluetoothInterface bluetooth("test.app");
bluetooth.SetBluetoothPermission(true);
std::vector<std::shared_ptr<mosis::BluetoothConnection>> 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
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<uint8_t> data = {0x01, 0x02, 0x03};
bool sent = conn->Send(data, err);
EXPECT_TRUE(sent);
// Test receive via simulation
std::vector<uint8_t> received;
bool got_data = false;
conn->SetOnData([&](const std::vector<uint8_t>& d) {
got_data = true;
received = d;
});
std::vector<uint8_t> 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
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
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:
Test_BluetoothRequiresPermission- Permission requiredTest_BluetoothRequiresUserConsent- User consent required for discoveryTest_BluetoothDiscoveryWorks- Discovery returns devicesTest_BluetoothConnectionLimit- Max 5 connections enforcedTest_BluetoothSendReceive- Send/receive data worksTest_BluetoothCleansUpOnShutdown- Cleanup on shutdownTest_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:
- Use BluetoothAdapter via JNI
- Handle BLUETOOTH_SCAN and BLUETOOTH_CONNECT permissions
- Use BluetoothSocket for RFCOMM connections
- Support BLE via BluetoothGatt
Security Considerations
- Permission required:
bluetoothpermission needed for all operations - User consent: Discovery requires active user gesture
- Limited device info: Only name and address exposed (no UUIDs)
- Connection limit: Max 5 connections per app
- Cleanup: All connections closed when app stops
Privacy Features
- No service UUIDs: Prevents detailed device fingerprinting
- No RSSI: Prevents proximity tracking
- User consent: Discovery requires explicit user action
- Audit logging: All Bluetooth operations logged
Next Steps
After Milestone 16 passes:
- Milestone 17: Virtual Hardware - Contacts