Files
MosisService/docs/SANDBOX_MILESTONE_16.md

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: 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

// 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
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;
}
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;
}
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 required
  • Test_BluetoothRequiresUserConsent - User consent required for discovery
  • Test_BluetoothDiscoveryWorks - Discovery returns devices
  • Test_BluetoothConnectionLimit - Max 5 connections enforced
  • Test_BluetoothSendReceive - Send/receive data works
  • Test_BluetoothCleansUpOnShutdown - Cleanup on shutdown
  • 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