# 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 ```cpp // location_interface.h #pragma once #include #include #include #include #include #include #include #include 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; using ErrorCallback = std::function; 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 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 success, std::function error, std::string& out_error ); // Watch position (continuous) // Returns watch on success, nullptr on failure std::shared_ptr 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 GetWatch(int id); // For rate limiting bool CheckRateLimit(); private: std::string m_app_id; std::unordered_map> 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 ```cpp 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 ```lua -- 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 ```lua -- 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 ```cpp 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 ```cpp 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 ```cpp 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 ```cpp 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 ```cpp bool Test_LocationWatchLimit(std::string& error_msg) { mosis::LocationInterface location("test.app"); location.SetCoarsePermission(true); std::vector> 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 ```cpp 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 ```cpp 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: - [x] `Test_LocationRequiresPermission` - Permission required - [x] `Test_LocationCoarseReducesPrecision` - Coarse reduces precision - [x] `Test_LocationRateLimits` - Rate limiting enforced - [x] `Test_LocationWatchPosition` - Watch mode works - [x] `Test_LocationWatchLimit` - Watch limit enforced - [x] `Test_LocationCleansUpOnShutdown` - Cleanup on shutdown - [x] `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