567 lines
15 KiB
Markdown
567 lines
15 KiB
Markdown
# 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 <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
|
|
|
|
```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::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
|
|
|
|
```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::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
|
|
|
|
```cpp
|
|
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
|
|
|
|
```cpp
|
|
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
|
|
|
|
```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<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
|
|
|
|
```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
|