move docs to docs/ folder, merge architecture files, update references
This commit is contained in:
561
docs/SANDBOX_MILESTONE_14.md
Normal file
561
docs/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
|
||||
Reference in New Issue
Block a user