implement Milestone 14: location interface with coarse/fine precision and rate limiting
This commit is contained in:
@@ -650,8 +650,9 @@ TEST(AudioOutput, LimitsConcurrent);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Milestone 14: Virtual Hardware - Location
|
## Milestone 14: Virtual Hardware - Location ✅
|
||||||
|
|
||||||
|
**Status**: Complete
|
||||||
**Goal**: Location with privacy controls.
|
**Goal**: Location with privacy controls.
|
||||||
**Estimated Files**: 1 new file
|
**Estimated Files**: 1 new file
|
||||||
|
|
||||||
|
|||||||
561
SANDBOX_MILESTONE_14.md
Normal file
561
SANDBOX_MILESTONE_14.md
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
# 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 <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
|
||||||
|
|
||||||
|
```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<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
|
||||||
|
|
||||||
|
```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
|
||||||
@@ -27,6 +27,7 @@ add_library(mosis-sandbox STATIC
|
|||||||
../src/main/cpp/sandbox/camera_interface.cpp
|
../src/main/cpp/sandbox/camera_interface.cpp
|
||||||
../src/main/cpp/sandbox/microphone_interface.cpp
|
../src/main/cpp/sandbox/microphone_interface.cpp
|
||||||
../src/main/cpp/sandbox/audio_output.cpp
|
../src/main/cpp/sandbox/audio_output.cpp
|
||||||
|
../src/main/cpp/sandbox/location_interface.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(mosis-sandbox PUBLIC
|
target_include_directories(mosis-sandbox PUBLIC
|
||||||
../src/main/cpp/sandbox
|
../src/main/cpp/sandbox
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "camera_interface.h"
|
#include "camera_interface.h"
|
||||||
#include "microphone_interface.h"
|
#include "microphone_interface.h"
|
||||||
#include "audio_output.h"
|
#include "audio_output.h"
|
||||||
|
#include "location_interface.h"
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -2323,6 +2324,201 @@ bool Test_AudioLuaIntegration(std::string& error_msg) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// MILESTONE 14: Location
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// MAIN
|
// MAIN
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -2488,6 +2684,15 @@ int main(int argc, char* argv[]) {
|
|||||||
harness.AddTest("AudioStopsOnShutdown", Test_AudioStopsOnShutdown);
|
harness.AddTest("AudioStopsOnShutdown", Test_AudioStopsOnShutdown);
|
||||||
harness.AddTest("AudioLuaIntegration", Test_AudioLuaIntegration);
|
harness.AddTest("AudioLuaIntegration", Test_AudioLuaIntegration);
|
||||||
|
|
||||||
|
// Milestone 14: Location
|
||||||
|
harness.AddTest("LocationRequiresPermission", Test_LocationRequiresPermission);
|
||||||
|
harness.AddTest("LocationCoarseReducesPrecision", Test_LocationCoarseReducesPrecision);
|
||||||
|
harness.AddTest("LocationRateLimits", Test_LocationRateLimits);
|
||||||
|
harness.AddTest("LocationWatchPosition", Test_LocationWatchPosition);
|
||||||
|
harness.AddTest("LocationWatchLimit", Test_LocationWatchLimit);
|
||||||
|
harness.AddTest("LocationCleansUpOnShutdown", Test_LocationCleansUpOnShutdown);
|
||||||
|
harness.AddTest("LocationLuaIntegration", Test_LocationLuaIntegration);
|
||||||
|
|
||||||
// Run tests
|
// Run tests
|
||||||
auto results = harness.Run(filter);
|
auto results = harness.Run(filter);
|
||||||
|
|
||||||
|
|||||||
533
src/main/cpp/sandbox/location_interface.cpp
Normal file
533
src/main/cpp/sandbox/location_interface.cpp
Normal file
@@ -0,0 +1,533 @@
|
|||||||
|
// location_interface.cpp - Location interface implementation for Lua sandbox
|
||||||
|
// Milestone 14: Location access with privacy controls and precision levels
|
||||||
|
|
||||||
|
#include "location_interface.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <lua.h>
|
||||||
|
#include <lauxlib.h>
|
||||||
|
#include <lualib.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// LocationWatch Implementation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
LocationWatch::LocationWatch(int id, const LocationOptions& options)
|
||||||
|
: m_id(id)
|
||||||
|
, m_options(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationWatch::~LocationWatch() {
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocationWatch::Start() {
|
||||||
|
m_active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocationWatch::Stop() {
|
||||||
|
m_active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocationWatch::SimulatePosition(const Position& pos) {
|
||||||
|
if (m_active && m_on_position) {
|
||||||
|
m_on_position(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocationWatch::SimulateError(LocationError error, const std::string& message) {
|
||||||
|
if (m_active && m_on_error) {
|
||||||
|
m_on_error(error, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// LocationInterface Implementation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
LocationInterface::LocationInterface(const std::string& app_id)
|
||||||
|
: m_app_id(app_id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationInterface::~LocationInterface() {
|
||||||
|
Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LocationInterface::CheckRateLimit() {
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
if (m_first_request) {
|
||||||
|
m_first_request = false;
|
||||||
|
m_last_request_time = now;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
now - m_last_request_time
|
||||||
|
).count();
|
||||||
|
|
||||||
|
if (elapsed < RATE_LIMIT_MS) {
|
||||||
|
return false; // Rate limited
|
||||||
|
}
|
||||||
|
|
||||||
|
m_last_request_time = now;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LocationInterface::GetCurrentPosition(
|
||||||
|
const LocationOptions& options,
|
||||||
|
std::function<void(const Position&)> success,
|
||||||
|
std::function<void(LocationError, const std::string&)> error,
|
||||||
|
std::string& out_error
|
||||||
|
) {
|
||||||
|
// Check permission
|
||||||
|
if (!m_has_fine_permission && !m_has_coarse_permission) {
|
||||||
|
out_error = "Location permission denied";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check rate limit
|
||||||
|
if (!CheckRateLimit()) {
|
||||||
|
out_error = "Location request rate limited (1 per second)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_mock_mode) {
|
||||||
|
// In mock mode, immediately return the mock position
|
||||||
|
Position result = ApplyPrecision(m_mock_position);
|
||||||
|
result.timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::system_clock::now().time_since_epoch()
|
||||||
|
).count();
|
||||||
|
if (success) {
|
||||||
|
success(result);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real implementation would use platform APIs here
|
||||||
|
out_error = "Real location not implemented";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<LocationWatch> LocationInterface::WatchPosition(
|
||||||
|
const LocationOptions& options,
|
||||||
|
std::string& error
|
||||||
|
) {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if (!m_has_fine_permission && !m_has_coarse_permission) {
|
||||||
|
error = "Location permission denied";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up stopped watches first
|
||||||
|
CleanupStoppedWatches();
|
||||||
|
|
||||||
|
// Check watch limit
|
||||||
|
if (m_watches.size() >= MAX_WATCHES) {
|
||||||
|
error = "Maximum watch limit reached (5)";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create watch
|
||||||
|
int id = m_next_watch_id++;
|
||||||
|
auto watch = std::make_shared<LocationWatch>(id, options);
|
||||||
|
watch->Start();
|
||||||
|
|
||||||
|
m_watches[id] = watch;
|
||||||
|
return watch;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocationInterface::ClearWatch(int watch_id) {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
auto it = m_watches.find(watch_id);
|
||||||
|
if (it != m_watches.end()) {
|
||||||
|
it->second->Stop();
|
||||||
|
m_watches.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocationInterface::ClearAllWatches() {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
for (auto& [id, watch] : m_watches) {
|
||||||
|
watch->Stop();
|
||||||
|
}
|
||||||
|
m_watches.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t LocationInterface::GetActiveWatchCount() const {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
size_t count = 0;
|
||||||
|
for (const auto& [id, watch] : m_watches) {
|
||||||
|
if (watch->IsActive()) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocationInterface::Shutdown() {
|
||||||
|
ClearAllWatches();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<LocationWatch> LocationInterface::GetWatch(int id) {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
|
auto it = m_watches.find(id);
|
||||||
|
if (it != m_watches.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LocationInterface::CleanupStoppedWatches() {
|
||||||
|
// Called with lock held
|
||||||
|
for (auto it = m_watches.begin(); it != m_watches.end();) {
|
||||||
|
if (!it->second->IsActive()) {
|
||||||
|
it = m_watches.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Lua API Implementation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Weak reference to watch stored in userdata
|
||||||
|
struct LuaWatchRef {
|
||||||
|
std::weak_ptr<LocationWatch> watch;
|
||||||
|
LocationInterface* location;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char* WATCH_METATABLE = "mosis.LocationWatch";
|
||||||
|
|
||||||
|
static LuaWatchRef* GetWatchRef(lua_State* L, int idx) {
|
||||||
|
return static_cast<LuaWatchRef*>(luaL_checkudata(L, idx, WATCH_METATABLE));
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::shared_ptr<LocationWatch> GetWatch(lua_State* L, int idx) {
|
||||||
|
auto ref = GetWatchRef(L, idx);
|
||||||
|
return ref->watch.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch methods
|
||||||
|
|
||||||
|
static int LuaWatch_stop(lua_State* L) {
|
||||||
|
auto ref = GetWatchRef(L, 1);
|
||||||
|
auto watch = ref->watch.lock();
|
||||||
|
if (watch && ref->location) {
|
||||||
|
ref->location->ClearWatch(watch->GetId());
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int LuaWatch_getId(lua_State* L) {
|
||||||
|
auto watch = GetWatch(L, 1);
|
||||||
|
if (watch) {
|
||||||
|
lua_pushinteger(L, watch->GetId());
|
||||||
|
} else {
|
||||||
|
lua_pushinteger(L, 0);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int LuaWatch_isActive(lua_State* L) {
|
||||||
|
auto watch = GetWatch(L, 1);
|
||||||
|
lua_pushboolean(L, watch && watch->IsActive());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int LuaWatch_gc(lua_State* L) {
|
||||||
|
auto ref = GetWatchRef(L, 1);
|
||||||
|
ref->~LuaWatchRef();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const luaL_Reg watch_methods[] = {
|
||||||
|
{"stop", LuaWatch_stop},
|
||||||
|
{"getId", LuaWatch_getId},
|
||||||
|
{"isActive", LuaWatch_isActive},
|
||||||
|
{nullptr, nullptr}
|
||||||
|
};
|
||||||
|
|
||||||
|
static void CreateWatchMetatable(lua_State* L) {
|
||||||
|
luaL_newmetatable(L, WATCH_METATABLE);
|
||||||
|
|
||||||
|
// __index = methods table
|
||||||
|
lua_newtable(L);
|
||||||
|
luaL_setfuncs(L, watch_methods, 0);
|
||||||
|
lua_setfield(L, -2, "__index");
|
||||||
|
|
||||||
|
// __gc
|
||||||
|
lua_pushcfunction(L, LuaWatch_gc);
|
||||||
|
lua_setfield(L, -2, "__gc");
|
||||||
|
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PushWatch(lua_State* L, std::shared_ptr<LocationWatch> watch, LocationInterface* location) {
|
||||||
|
auto ref = static_cast<LuaWatchRef*>(lua_newuserdata(L, sizeof(LuaWatchRef)));
|
||||||
|
new (ref) LuaWatchRef{watch, location};
|
||||||
|
luaL_setmetatable(L, WATCH_METATABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* LocationErrorToString(LocationError err) {
|
||||||
|
switch (err) {
|
||||||
|
case LocationError::PermissionDenied: return "PERMISSION_DENIED";
|
||||||
|
case LocationError::PositionUnavailable: return "POSITION_UNAVAILABLE";
|
||||||
|
case LocationError::Timeout: return "TIMEOUT";
|
||||||
|
case LocationError::RateLimited: return "RATE_LIMITED";
|
||||||
|
case LocationError::WatchLimitReached: return "WATCH_LIMIT_REACHED";
|
||||||
|
default: return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// location.getCurrentPosition(successCallback, errorCallback, options)
|
||||||
|
static int LuaLocation_getCurrentPosition(lua_State* L) {
|
||||||
|
auto location = static_cast<LocationInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
|
||||||
|
if (!location) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_pushstring(L, "Location interface not available");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate callbacks
|
||||||
|
if (!lua_isfunction(L, 1)) {
|
||||||
|
return luaL_error(L, "First argument must be a success callback function");
|
||||||
|
}
|
||||||
|
if (!lua_isfunction(L, 2) && !lua_isnil(L, 2)) {
|
||||||
|
return luaL_error(L, "Second argument must be an error callback function or nil");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse options from third argument (optional table)
|
||||||
|
LocationOptions options;
|
||||||
|
if (lua_istable(L, 3)) {
|
||||||
|
lua_getfield(L, 3, "enableHighAccuracy");
|
||||||
|
if (lua_isboolean(L, -1)) {
|
||||||
|
options.enable_high_accuracy = lua_toboolean(L, -1);
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
lua_getfield(L, 3, "timeout");
|
||||||
|
if (lua_isnumber(L, -1)) {
|
||||||
|
options.timeout_ms = static_cast<int>(lua_tointeger(L, -1));
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
lua_getfield(L, 3, "maxAge");
|
||||||
|
if (lua_isnumber(L, -1)) {
|
||||||
|
options.max_age_ms = static_cast<int>(lua_tointeger(L, -1));
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store callbacks in registry for later use
|
||||||
|
lua_pushvalue(L, 1); // success callback
|
||||||
|
int success_ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||||
|
|
||||||
|
int error_ref = LUA_NOREF;
|
||||||
|
if (lua_isfunction(L, 2)) {
|
||||||
|
lua_pushvalue(L, 2); // error callback
|
||||||
|
error_ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
bool ok = location->GetCurrentPosition(
|
||||||
|
options,
|
||||||
|
[L, success_ref](const Position& pos) {
|
||||||
|
// Call success callback
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, success_ref);
|
||||||
|
if (lua_isfunction(L, -1)) {
|
||||||
|
// Create position table
|
||||||
|
lua_newtable(L);
|
||||||
|
lua_pushnumber(L, pos.latitude);
|
||||||
|
lua_setfield(L, -2, "latitude");
|
||||||
|
lua_pushnumber(L, pos.longitude);
|
||||||
|
lua_setfield(L, -2, "longitude");
|
||||||
|
lua_pushnumber(L, pos.accuracy);
|
||||||
|
lua_setfield(L, -2, "accuracy");
|
||||||
|
lua_pushnumber(L, pos.altitude);
|
||||||
|
lua_setfield(L, -2, "altitude");
|
||||||
|
lua_pushnumber(L, pos.altitude_accuracy);
|
||||||
|
lua_setfield(L, -2, "altitudeAccuracy");
|
||||||
|
lua_pushnumber(L, pos.heading);
|
||||||
|
lua_setfield(L, -2, "heading");
|
||||||
|
lua_pushnumber(L, pos.speed);
|
||||||
|
lua_setfield(L, -2, "speed");
|
||||||
|
lua_pushinteger(L, pos.timestamp);
|
||||||
|
lua_setfield(L, -2, "timestamp");
|
||||||
|
|
||||||
|
lua_pcall(L, 1, 0, 0);
|
||||||
|
} else {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
luaL_unref(L, LUA_REGISTRYINDEX, success_ref);
|
||||||
|
},
|
||||||
|
[L, error_ref](LocationError error, const std::string& message) {
|
||||||
|
if (error_ref != LUA_NOREF) {
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, error_ref);
|
||||||
|
if (lua_isfunction(L, -1)) {
|
||||||
|
lua_pushstring(L, LocationErrorToString(error));
|
||||||
|
lua_pushstring(L, message.c_str());
|
||||||
|
lua_pcall(L, 2, 0, 0);
|
||||||
|
} else {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
luaL_unref(L, LUA_REGISTRYINDEX, error_ref);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
err
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
// Clean up refs on failure
|
||||||
|
luaL_unref(L, LUA_REGISTRYINDEX, success_ref);
|
||||||
|
if (error_ref != LUA_NOREF) {
|
||||||
|
luaL_unref(L, LUA_REGISTRYINDEX, error_ref);
|
||||||
|
}
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_pushstring(L, err.c_str());
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_pushboolean(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// location.watchPosition(successCallback, errorCallback, options)
|
||||||
|
static int LuaLocation_watchPosition(lua_State* L) {
|
||||||
|
auto location = static_cast<LocationInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
|
||||||
|
if (!location) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_pushstring(L, "Location interface not available");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse options from third argument (optional table)
|
||||||
|
LocationOptions options;
|
||||||
|
if (lua_istable(L, 3)) {
|
||||||
|
lua_getfield(L, 3, "enableHighAccuracy");
|
||||||
|
if (lua_isboolean(L, -1)) {
|
||||||
|
options.enable_high_accuracy = lua_toboolean(L, -1);
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
lua_getfield(L, 3, "timeout");
|
||||||
|
if (lua_isnumber(L, -1)) {
|
||||||
|
options.timeout_ms = static_cast<int>(lua_tointeger(L, -1));
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto watch = location->WatchPosition(options, err);
|
||||||
|
|
||||||
|
if (!watch) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_pushstring(L, err.c_str());
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
PushWatch(L, watch, location);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// location.clearWatch(watchId)
|
||||||
|
static int LuaLocation_clearWatch(lua_State* L) {
|
||||||
|
auto location = static_cast<LocationInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
|
||||||
|
if (location) {
|
||||||
|
int watch_id = static_cast<int>(luaL_checkinteger(L, 1));
|
||||||
|
location->ClearWatch(watch_id);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// location.getWatchCount()
|
||||||
|
static int LuaLocation_getWatchCount(lua_State* L) {
|
||||||
|
auto location = static_cast<LocationInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
|
||||||
|
if (location) {
|
||||||
|
lua_pushinteger(L, static_cast<lua_Integer>(location->GetActiveWatchCount()));
|
||||||
|
} else {
|
||||||
|
lua_pushinteger(L, 0);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to set a global in the real _G (bypassing any proxy)
|
||||||
|
static void SetGlobalInRealG(lua_State* L, const char* name) {
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
|
||||||
|
|
||||||
|
if (lua_getmetatable(L, -1)) {
|
||||||
|
lua_getfield(L, -1, "__index");
|
||||||
|
if (lua_istable(L, -1)) {
|
||||||
|
lua_pushvalue(L, -4);
|
||||||
|
lua_setfield(L, -2, name);
|
||||||
|
lua_pop(L, 4);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lua_pop(L, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_pushvalue(L, -2);
|
||||||
|
lua_setfield(L, -2, name);
|
||||||
|
lua_pop(L, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RegisterLocationAPI(lua_State* L, LocationInterface* location) {
|
||||||
|
// Create watch metatable
|
||||||
|
CreateWatchMetatable(L);
|
||||||
|
|
||||||
|
// Create location table
|
||||||
|
lua_newtable(L);
|
||||||
|
|
||||||
|
// location.getCurrentPosition
|
||||||
|
lua_pushlightuserdata(L, location);
|
||||||
|
lua_pushcclosure(L, LuaLocation_getCurrentPosition, 1);
|
||||||
|
lua_setfield(L, -2, "getCurrentPosition");
|
||||||
|
|
||||||
|
// location.watchPosition
|
||||||
|
lua_pushlightuserdata(L, location);
|
||||||
|
lua_pushcclosure(L, LuaLocation_watchPosition, 1);
|
||||||
|
lua_setfield(L, -2, "watchPosition");
|
||||||
|
|
||||||
|
// location.clearWatch
|
||||||
|
lua_pushlightuserdata(L, location);
|
||||||
|
lua_pushcclosure(L, LuaLocation_clearWatch, 1);
|
||||||
|
lua_setfield(L, -2, "clearWatch");
|
||||||
|
|
||||||
|
// location.getWatchCount
|
||||||
|
lua_pushlightuserdata(L, location);
|
||||||
|
lua_pushcclosure(L, LuaLocation_getWatchCount, 1);
|
||||||
|
lua_setfield(L, -2, "getWatchCount");
|
||||||
|
|
||||||
|
// Set as global (bypasses sandbox proxy)
|
||||||
|
SetGlobalInRealG(L, "location");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
152
src/main/cpp/sandbox/location_interface.h
Normal file
152
src/main/cpp/sandbox/location_interface.h
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
// location_interface.h - Location interface for Lua sandbox
|
||||||
|
// Milestone 14: Location access with privacy controls and precision levels
|
||||||
|
|
||||||
|
#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,
|
||||||
|
WatchLimitReached
|
||||||
|
};
|
||||||
|
|
||||||
|
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 - check and consume
|
||||||
|
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;
|
||||||
|
bool m_first_request = true;
|
||||||
|
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user