Files
MosisService/SANDBOX_MILESTONE_14.md

15 KiB

Milestone 14: Virtual Hardware - Location

Status: Complete Goal: Location access with privacy controls and precision levels.


Overview

This milestone implements secure location access for Lua apps:

  • Two permission levels: coarse (city-level) and fine (GPS precision)
  • Rate limiting to prevent tracking abuse
  • Automatic precision reduction for coarse permission
  • Watch mode for continuous updates
  • Automatic cleanup on app stop

Key Deliverables

  1. LocationInterface class - Location access management
  2. LocationWatch class - Continuous position monitoring
  3. Lua location API - location.getCurrentPosition(), location.watchPosition()
  4. Precision enforcement - Coarse vs fine accuracy

File Structure

src/main/cpp/sandbox/
├── location_interface.h      # NEW - Location API header
└── location_interface.cpp    # NEW - Location implementation

Implementation Details

1. LocationInterface Class

// location_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 Position {
    double latitude = 0.0;
    double longitude = 0.0;
    double accuracy = 0.0;         // meters
    double altitude = 0.0;         // meters (optional)
    double altitude_accuracy = 0.0; // meters (optional)
    double heading = 0.0;          // degrees (optional)
    double speed = 0.0;            // m/s (optional)
    int64_t timestamp = 0;         // milliseconds since epoch
};

struct LocationOptions {
    bool enable_high_accuracy = false;
    int timeout_ms = 30000;        // max time to wait
    int max_age_ms = 0;            // accept cached position this old
};

enum class LocationError {
    None,
    PermissionDenied,
    PositionUnavailable,
    Timeout,
    RateLimited,
    AlreadyWatching
};

class LocationWatch {
public:
    using PositionCallback = std::function<void(const Position&)>;
    using ErrorCallback = std::function<void(LocationError, const std::string&)>;

    LocationWatch(int id, const LocationOptions& options);
    ~LocationWatch();

    int GetId() const { return m_id; }
    bool IsActive() const { return m_active; }

    void Start();
    void Stop();

    void SetOnPosition(PositionCallback cb) { m_on_position = std::move(cb); }
    void SetOnError(ErrorCallback cb) { m_on_error = std::move(cb); }

    // For mock mode - simulate position update
    void SimulatePosition(const Position& pos);
    void SimulateError(LocationError error, const std::string& message);

private:
    int m_id;
    LocationOptions m_options;
    std::atomic<bool> m_active{false};
    PositionCallback m_on_position;
    ErrorCallback m_on_error;
    mutable std::mutex m_mutex;
};

class LocationInterface {
public:
    LocationInterface(const std::string& app_id);
    ~LocationInterface();

    // Check if app has location permission
    bool HasFinePermission() const { return m_has_fine_permission; }
    bool HasCoarsePermission() const { return m_has_coarse_permission; }
    void SetFinePermission(bool granted) { m_has_fine_permission = granted; }
    void SetCoarsePermission(bool granted) { m_has_coarse_permission = granted; }

    // Get current position (one-shot)
    // Returns true if request started, false on error
    bool GetCurrentPosition(
        const LocationOptions& options,
        std::function<void(const Position&)> success,
        std::function<void(LocationError, const std::string&)> error,
        std::string& out_error
    );

    // Watch position (continuous)
    // Returns watch on success, nullptr on failure
    std::shared_ptr<LocationWatch> WatchPosition(
        const LocationOptions& options,
        std::string& error
    );

    // Stop a specific watch
    void ClearWatch(int watch_id);

    // Stop all watches for this app
    void ClearAllWatches();

    // Get active watch count
    size_t GetActiveWatchCount() 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 the position to return
    void SetMockPosition(const Position& pos) { m_mock_position = pos; }

    // Get watch by ID
    std::shared_ptr<LocationWatch> GetWatch(int id);

    // For rate limiting
    bool CheckRateLimit();

private:
    std::string m_app_id;
    std::unordered_map<int, std::shared_ptr<LocationWatch>> m_watches;
    mutable std::mutex m_mutex;
    bool m_mock_mode = true;
    bool m_has_fine_permission = false;
    bool m_has_coarse_permission = false;
    Position m_mock_position;
    int m_next_watch_id = 1;
    std::chrono::steady_clock::time_point m_last_request_time;

    static constexpr int MAX_WATCHES = 5;
    static constexpr int RATE_LIMIT_MS = 1000;  // 1 request per second
    static constexpr double COARSE_ACCURACY_M = 1000.0;  // 1km for coarse

    // Apply precision reduction for coarse permission
    Position ApplyPrecision(const Position& pos) const;

    void CleanupStoppedWatches();
};

// Register location.* APIs as globals
void RegisterLocationAPI(lua_State* L, LocationInterface* location);

} // namespace mosis

2. Permission Levels

Two permission levels with different accuracy:

  • location.fine - Full GPS precision (meters)
  • location.coarse - City-level precision (~1km)

3. Precision Reduction for Coarse

When only coarse permission is granted:

  • Latitude/longitude rounded to ~1km grid
  • Accuracy reported as minimum 1000 meters
  • Altitude, heading, speed not provided
Position LocationInterface::ApplyPrecision(const Position& pos) const {
    if (m_has_fine_permission) {
        return pos;  // Full precision
    }

    // Coarse: round to ~1km (0.01 degrees ≈ 1.1km)
    Position coarse = pos;
    coarse.latitude = std::round(pos.latitude * 100.0) / 100.0;
    coarse.longitude = std::round(pos.longitude * 100.0) / 100.0;
    coarse.accuracy = std::max(pos.accuracy, COARSE_ACCURACY_M);
    coarse.altitude = 0.0;
    coarse.altitude_accuracy = 0.0;
    coarse.heading = 0.0;
    coarse.speed = 0.0;
    return coarse;
}

4. Rate Limiting

Maximum 1 location request per second per app:

  • Prevents continuous tracking abuse
  • Applies to both getCurrentPosition and watchPosition updates

5. Lua API

-- Get current position (one-shot)
location.getCurrentPosition(function(pos)
    print("Lat:", pos.latitude)
    print("Lon:", pos.longitude)
    print("Accuracy:", pos.accuracy, "meters")
    print("Timestamp:", pos.timestamp)
end, function(error, message)
    print("Error:", error, message)
end, {
    enableHighAccuracy = true,  -- Request fine permission
    timeout = 30000,            -- Max wait time in ms
    maxAge = 0                  -- Don't accept cached position
})

-- Watch position (continuous updates)
local watch = location.watchPosition(function(pos)
    print("Updated position:", pos.latitude, pos.longitude)
end, function(error, message)
    print("Watch error:", error, message)
end, {
    enableHighAccuracy = false  -- Use coarse permission
})

-- Stop watching
watch:stop()

-- Or clear by ID
location.clearWatch(watch:getId())

-- Get active watch count
local count = location.getWatchCount()

6. Error Codes

-- Error values passed to error callback
"PERMISSION_DENIED"     -- No location permission
"POSITION_UNAVAILABLE"  -- Location not available
"TIMEOUT"               -- Request timed out
"RATE_LIMITED"          -- Too many requests
"ALREADY_WATCHING"      -- Max watches reached

Test Cases

Test 1: Requires Permission

bool Test_LocationRequiresPermission(std::string& error_msg) {
    mosis::LocationInterface location("test.app");
    // No permissions granted

    mosis::LocationOptions options;
    std::string err;
    bool started = location.GetCurrentPosition(
        options,
        [](const mosis::Position&) {},
        [](mosis::LocationError, const std::string&) {},
        err
    );

    EXPECT_TRUE(!started);
    EXPECT_TRUE(err.find("permission") != std::string::npos);

    return true;
}

Test 2: Coarse Reduces Precision

bool Test_LocationCoarseReducesPrecision(std::string& error_msg) {
    mosis::LocationInterface location("test.app");
    location.SetCoarsePermission(true);  // Only coarse, not fine

    // Set mock position with high precision
    mosis::Position precise;
    precise.latitude = 37.7749295;
    precise.longitude = -122.4194155;
    precise.accuracy = 5.0;
    precise.altitude = 100.0;
    location.SetMockPosition(precise);

    bool got_position = false;
    mosis::Position received;

    mosis::LocationOptions options;
    std::string err;
    location.GetCurrentPosition(
        options,
        [&](const mosis::Position& pos) {
            got_position = true;
            received = pos;
        },
        [](mosis::LocationError, const std::string&) {},
        err
    );

    // Trigger mock callback
    // In mock mode, position is returned immediately

    EXPECT_TRUE(got_position);
    // Should be rounded to ~0.01 degrees
    EXPECT_TRUE(std::abs(received.latitude - 37.77) < 0.001);
    EXPECT_TRUE(std::abs(received.longitude - (-122.42)) < 0.001);
    // Accuracy should be at least 1000m
    EXPECT_TRUE(received.accuracy >= 1000.0);
    // Fine details should be zeroed
    EXPECT_TRUE(received.altitude == 0.0);

    return true;
}

Test 3: Rate Limiting

bool Test_LocationRateLimits(std::string& error_msg) {
    mosis::LocationInterface location("test.app");
    location.SetCoarsePermission(true);

    mosis::Position pos;
    pos.latitude = 37.77;
    pos.longitude = -122.42;
    location.SetMockPosition(pos);

    mosis::LocationOptions options;

    // First request should succeed
    std::string err;
    bool ok1 = location.GetCurrentPosition(
        options,
        [](const mosis::Position&) {},
        [](mosis::LocationError, const std::string&) {},
        err
    );
    EXPECT_TRUE(ok1);

    // Immediate second request should be rate limited
    bool ok2 = location.GetCurrentPosition(
        options,
        [](const mosis::Position&) {},
        [](mosis::LocationError, const std::string&) {},
        err
    );
    EXPECT_TRUE(!ok2);
    EXPECT_TRUE(err.find("rate") != std::string::npos ||
                err.find("limit") != std::string::npos);

    return true;
}

Test 4: Watch Position

bool Test_LocationWatchPosition(std::string& error_msg) {
    mosis::LocationInterface location("test.app");
    location.SetFinePermission(true);

    std::string err;
    auto watch = location.WatchPosition({}, err);

    EXPECT_TRUE(watch != nullptr);
    EXPECT_TRUE(watch->IsActive());
    EXPECT_TRUE(location.GetActiveWatchCount() == 1);

    watch->Stop();
    EXPECT_TRUE(!watch->IsActive());

    return true;
}

Test 5: Watch Limit

bool Test_LocationWatchLimit(std::string& error_msg) {
    mosis::LocationInterface location("test.app");
    location.SetCoarsePermission(true);

    std::vector<std::shared_ptr<mosis::LocationWatch>> watches;
    std::string err;

    // Create MAX_WATCHES (5) watches
    for (int i = 0; i < 5; i++) {
        auto watch = location.WatchPosition({}, err);
        EXPECT_TRUE(watch != nullptr);
        watches.push_back(watch);
    }

    // 6th should fail
    auto extra = location.WatchPosition({}, err);
    EXPECT_TRUE(extra == nullptr);
    EXPECT_TRUE(err.find("limit") != std::string::npos ||
                err.find("maximum") != std::string::npos);

    return true;
}

Test 6: Cleanup on Shutdown

bool Test_LocationCleansUpOnShutdown(std::string& error_msg) {
    mosis::LocationInterface location("test.app");
    location.SetCoarsePermission(true);

    std::string err;
    auto watch1 = location.WatchPosition({}, err);
    auto watch2 = location.WatchPosition({}, err);

    EXPECT_TRUE(location.GetActiveWatchCount() == 2);

    location.Shutdown();

    EXPECT_TRUE(location.GetActiveWatchCount() == 0);
    EXPECT_TRUE(!watch1->IsActive());
    EXPECT_TRUE(!watch2->IsActive());

    return true;
}

Test 7: Lua Integration

bool Test_LocationLuaIntegration(std::string& error_msg) {
    SandboxContext ctx = TestContext();
    LuaSandbox sandbox(ctx);

    mosis::LocationInterface location("test.app");
    mosis::RegisterLocationAPI(sandbox.GetState(), &location);

    std::string script = R"lua(
        -- Test that location global exists
        if not location then
            error("location global not found")
        end
        if not location.getCurrentPosition then
            error("location.getCurrentPosition not found")
        end
        if not location.watchPosition then
            error("location.watchPosition not found")
        end
        if not location.clearWatch then
            error("location.clearWatch not found")
        end
        if not location.getWatchCount then
            error("location.getWatchCount not found")
        end

        -- Watch count should be 0 initially
        if location.getWatchCount() ~= 0 then
            error("should have no active watches initially")
        end
    )lua";

    bool ok = sandbox.LoadString(script, "location_test");
    if (!ok) {
        error_msg = "Lua test failed: " + sandbox.GetLastError();
        return false;
    }
    return true;
}

Acceptance Criteria

All tests must pass:

  • Test_LocationRequiresPermission - Permission required
  • Test_LocationCoarseReducesPrecision - Coarse reduces precision
  • Test_LocationRateLimits - Rate limiting enforced
  • Test_LocationWatchPosition - Watch mode works
  • Test_LocationWatchLimit - Watch limit enforced
  • Test_LocationCleansUpOnShutdown - Cleanup on shutdown
  • Test_LocationLuaIntegration - Lua API works

Dependencies

  • Milestone 1 (LuaSandbox)
  • Milestone 2 (PermissionGate)
  • Milestone 3 (RateLimiter)

Notes

Desktop vs Android Implementation

For desktop testing, LocationInterface operates in mock mode:

  • Positions are set programmatically via SetMockPosition()
  • Watches track state but don't receive real updates
  • Rate limiting is enforced normally

On Android, the real implementation would:

  1. Use FusedLocationProviderClient via JNI
  2. Request appropriate permissions at runtime
  3. Handle GPS/network provider selection
  4. Battery-efficient location batching

Security Considerations

  1. Permission required: Either coarse or fine permission needed
  2. Precision enforcement: Coarse permission reduces accuracy to ~1km
  3. Rate limiting: 1 request per second prevents tracking abuse
  4. Watch limit: Max 5 watches per app prevents resource exhaustion
  5. Cleanup: All watches stopped when app stops

Privacy Features

  1. Coarse mode: City-level accuracy (~1km) for apps that don't need precision
  2. No background tracking: Location only available while app is active
  3. Audit logging: All location requests logged for security review

Next Steps

After Milestone 14 passes:

  1. Milestone 15: Virtual Hardware - Sensors