move docs to docs/ folder, merge architecture files, update references
This commit is contained in:
566
docs/SANDBOX_MILESTONE_16.md
Normal file
566
docs/SANDBOX_MILESTONE_16.md
Normal file
@@ -0,0 +1,566 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user