move docs to docs/ folder, merge architecture files, update references
This commit is contained in:
472
docs/SANDBOX_MILESTONE_17.md
Normal file
472
docs/SANDBOX_MILESTONE_17.md
Normal file
@@ -0,0 +1,472 @@
|
||||
# Milestone 17: Virtual Hardware - Contacts
|
||||
|
||||
**Status**: Complete
|
||||
**Goal**: Contact access with granular read/write permissions.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This milestone implements secure Contacts access for Lua apps:
|
||||
- Separate permissions: `contacts.read` and `contacts.write`
|
||||
- Limited contact fields: name, phone, email (no raw account data)
|
||||
- Query with search/pagination support
|
||||
- Mock mode for desktop testing
|
||||
- Maximum 100 results per query
|
||||
|
||||
### Key Deliverables
|
||||
|
||||
1. **ContactsInterface class** - Contact query and management
|
||||
2. **Contact struct** - Limited contact fields (privacy-preserving)
|
||||
3. **Lua contacts API** - `contacts.query()`, `contacts.add()`, `contacts.update()`, `contacts.delete()`
|
||||
4. **Privacy protection** - Limited fields, permission enforcement
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
src/main/cpp/sandbox/
|
||||
├── contacts_interface.h # NEW - Contacts API header
|
||||
└── contacts_interface.cpp # NEW - Contacts implementation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. ContactsInterface Class
|
||||
|
||||
```cpp
|
||||
// contacts_interface.h
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
struct lua_State;
|
||||
|
||||
namespace mosis {
|
||||
|
||||
struct Contact {
|
||||
int id = 0; // Internal ID
|
||||
std::string name; // Display name
|
||||
std::string phone; // Primary phone number
|
||||
std::string email; // Primary email
|
||||
};
|
||||
|
||||
struct QueryOptions {
|
||||
std::string search; // Search string (matches name, phone, email)
|
||||
int limit = 50; // Max results (capped at 100)
|
||||
int offset = 0; // Pagination offset
|
||||
};
|
||||
|
||||
enum class ContactsError {
|
||||
None,
|
||||
PermissionDenied,
|
||||
NotFound,
|
||||
InvalidData,
|
||||
LimitExceeded
|
||||
};
|
||||
|
||||
class ContactsInterface {
|
||||
public:
|
||||
ContactsInterface(const std::string& app_id);
|
||||
~ContactsInterface();
|
||||
|
||||
// Permission checks
|
||||
bool HasReadPermission() const { return m_has_read_permission; }
|
||||
void SetReadPermission(bool granted) { m_has_read_permission = granted; }
|
||||
|
||||
bool HasWritePermission() const { return m_has_write_permission; }
|
||||
void SetWritePermission(bool granted) { m_has_write_permission = granted; }
|
||||
|
||||
// Query contacts (requires read permission)
|
||||
std::vector<Contact> Query(const QueryOptions& options, std::string& error);
|
||||
|
||||
// Get single contact by ID (requires read permission)
|
||||
std::optional<Contact> GetById(int id, std::string& error);
|
||||
|
||||
// Add contact (requires write permission)
|
||||
int Add(const Contact& contact, std::string& error);
|
||||
|
||||
// Update contact (requires write permission)
|
||||
bool Update(const Contact& contact, std::string& error);
|
||||
|
||||
// Delete contact (requires write permission)
|
||||
bool Delete(int id, std::string& error);
|
||||
|
||||
// Get total contact count (requires read permission)
|
||||
int GetCount(std::string& error);
|
||||
|
||||
// For testing
|
||||
void SetMockMode(bool enabled) { m_mock_mode = enabled; }
|
||||
bool IsMockMode() const { return m_mock_mode; }
|
||||
|
||||
// Mock mode: set contacts directly
|
||||
void SetMockContacts(const std::vector<Contact>& contacts);
|
||||
|
||||
// Clear all mock contacts
|
||||
void ClearMockContacts();
|
||||
|
||||
private:
|
||||
std::string m_app_id;
|
||||
bool m_mock_mode = true;
|
||||
bool m_has_read_permission = false;
|
||||
bool m_has_write_permission = false;
|
||||
std::vector<Contact> m_mock_contacts;
|
||||
int m_next_id = 1;
|
||||
mutable std::mutex m_mutex;
|
||||
|
||||
static constexpr int MAX_RESULTS = 100;
|
||||
|
||||
bool MatchesSearch(const Contact& contact, const std::string& search) const;
|
||||
};
|
||||
|
||||
// Register contacts.* APIs as globals
|
||||
void RegisterContactsAPI(lua_State* L, ContactsInterface* contacts);
|
||||
|
||||
} // namespace mosis
|
||||
```
|
||||
|
||||
### 2. Permission Requirements
|
||||
|
||||
| Action | Permission Required |
|
||||
|--------|---------------------|
|
||||
| Query contacts | `contacts.read` |
|
||||
| Get contact by ID | `contacts.read` |
|
||||
| Get contact count | `contacts.read` |
|
||||
| Add contact | `contacts.write` |
|
||||
| Update contact | `contacts.write` |
|
||||
| Delete contact | `contacts.write` |
|
||||
|
||||
### 3. Privacy Protection
|
||||
|
||||
Contact fields exposed:
|
||||
- `id` - Internal identifier
|
||||
- `name` - Display name
|
||||
- `phone` - Primary phone number
|
||||
- `email` - Primary email address
|
||||
|
||||
NOT exposed:
|
||||
- Account type/name (Google, Exchange, etc.)
|
||||
- Contact photo
|
||||
- Multiple phone numbers/emails
|
||||
- Address, notes, organization
|
||||
- Social profiles
|
||||
- Custom fields
|
||||
- Sync metadata
|
||||
|
||||
### 4. Lua API
|
||||
|
||||
```lua
|
||||
-- Query contacts (requires contacts.read)
|
||||
local results = contacts.query({
|
||||
search = "John", -- optional: search name/phone/email
|
||||
limit = 20, -- optional: max results (default 50, max 100)
|
||||
offset = 0 -- optional: pagination offset
|
||||
})
|
||||
for _, c in ipairs(results) do
|
||||
print(c.id, c.name, c.phone, c.email)
|
||||
end
|
||||
|
||||
-- Get by ID (requires contacts.read)
|
||||
local contact = contacts.getById(123)
|
||||
if contact then
|
||||
print(contact.name)
|
||||
end
|
||||
|
||||
-- Get total count (requires contacts.read)
|
||||
local count = contacts.getCount()
|
||||
|
||||
-- Add contact (requires contacts.write)
|
||||
local newId = contacts.add({
|
||||
name = "John Doe",
|
||||
phone = "555-1234",
|
||||
email = "john@example.com"
|
||||
})
|
||||
|
||||
-- Update contact (requires contacts.write)
|
||||
local success = contacts.update({
|
||||
id = 123,
|
||||
name = "John Smith",
|
||||
phone = "555-5678",
|
||||
email = "john.smith@example.com"
|
||||
})
|
||||
|
||||
-- Delete contact (requires contacts.write)
|
||||
local success = contacts.delete(123)
|
||||
```
|
||||
|
||||
### 5. Error Handling
|
||||
|
||||
```lua
|
||||
-- Errors are raised via Lua errors
|
||||
-- Use pcall for safe error handling
|
||||
local ok, result = pcall(function()
|
||||
return contacts.query({search = "test"})
|
||||
end)
|
||||
if not ok then
|
||||
print("Error:", result) -- e.g., "permission denied: contacts.read required"
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Cases
|
||||
|
||||
### Test 1: Query Requires Read Permission
|
||||
|
||||
```cpp
|
||||
bool Test_ContactsRequiresReadPermission(std::string& error_msg) {
|
||||
mosis::ContactsInterface contacts("test.app");
|
||||
// No permission granted
|
||||
|
||||
std::string err;
|
||||
auto results = contacts.Query({}, err);
|
||||
|
||||
EXPECT_TRUE(results.empty());
|
||||
EXPECT_TRUE(err.find("permission") != std::string::npos);
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Test 2: Add Requires Write Permission
|
||||
|
||||
```cpp
|
||||
bool Test_ContactsRequiresWritePermission(std::string& error_msg) {
|
||||
mosis::ContactsInterface contacts("test.app");
|
||||
contacts.SetReadPermission(true); // Read OK, but not write
|
||||
|
||||
std::string err;
|
||||
mosis::Contact contact{"John Doe", "555-1234", "john@test.com"};
|
||||
int id = contacts.Add(contact, err);
|
||||
|
||||
EXPECT_TRUE(id == 0); // Failed
|
||||
EXPECT_TRUE(err.find("permission") != std::string::npos);
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Test 3: Query Returns Matching Contacts
|
||||
|
||||
```cpp
|
||||
bool Test_ContactsQueryWorks(std::string& error_msg) {
|
||||
mosis::ContactsInterface contacts("test.app");
|
||||
contacts.SetReadPermission(true);
|
||||
|
||||
// Set mock contacts
|
||||
std::vector<mosis::Contact> mockContacts = {
|
||||
{1, "John Doe", "555-1234", "john@test.com"},
|
||||
{2, "Jane Doe", "555-5678", "jane@test.com"},
|
||||
{3, "Bob Smith", "555-9999", "bob@test.com"}
|
||||
};
|
||||
contacts.SetMockContacts(mockContacts);
|
||||
|
||||
std::string err;
|
||||
auto results = contacts.Query({.search = "Doe"}, err);
|
||||
|
||||
EXPECT_TRUE(err.empty());
|
||||
EXPECT_TRUE(results.size() == 2); // John and Jane
|
||||
EXPECT_TRUE(results[0].name == "John Doe" || results[0].name == "Jane Doe");
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Test 4: Query Respects Limit
|
||||
|
||||
```cpp
|
||||
bool Test_ContactsQueryLimit(std::string& error_msg) {
|
||||
mosis::ContactsInterface contacts("test.app");
|
||||
contacts.SetReadPermission(true);
|
||||
|
||||
// Set 50 mock contacts
|
||||
std::vector<mosis::Contact> mockContacts;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
mockContacts.push_back({i + 1, "Contact " + std::to_string(i), "555-" + std::to_string(i), ""});
|
||||
}
|
||||
contacts.SetMockContacts(mockContacts);
|
||||
|
||||
std::string err;
|
||||
|
||||
// Query with limit
|
||||
auto results = contacts.Query({.limit = 10}, err);
|
||||
EXPECT_TRUE(results.size() == 10);
|
||||
|
||||
// Query with excessive limit (capped at MAX_RESULTS)
|
||||
results = contacts.Query({.limit = 200}, err);
|
||||
EXPECT_TRUE(results.size() == 50); // All contacts (< MAX_RESULTS)
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Test 5: Add/Update/Delete Works
|
||||
|
||||
```cpp
|
||||
bool Test_ContactsCRUD(std::string& error_msg) {
|
||||
mosis::ContactsInterface contacts("test.app");
|
||||
contacts.SetReadPermission(true);
|
||||
contacts.SetWritePermission(true);
|
||||
|
||||
std::string err;
|
||||
|
||||
// Add
|
||||
mosis::Contact newContact{0, "Test User", "555-1111", "test@test.com"};
|
||||
int id = contacts.Add(newContact, err);
|
||||
EXPECT_TRUE(id > 0);
|
||||
EXPECT_TRUE(err.empty());
|
||||
|
||||
// Read back
|
||||
auto contact = contacts.GetById(id, err);
|
||||
EXPECT_TRUE(contact.has_value());
|
||||
EXPECT_TRUE(contact->name == "Test User");
|
||||
|
||||
// Update
|
||||
contact->name = "Updated User";
|
||||
contact->phone = "555-2222";
|
||||
bool updated = contacts.Update(*contact, err);
|
||||
EXPECT_TRUE(updated);
|
||||
|
||||
// Verify update
|
||||
contact = contacts.GetById(id, err);
|
||||
EXPECT_TRUE(contact->name == "Updated User");
|
||||
EXPECT_TRUE(contact->phone == "555-2222");
|
||||
|
||||
// Delete
|
||||
bool deleted = contacts.Delete(id, err);
|
||||
EXPECT_TRUE(deleted);
|
||||
|
||||
// Verify deleted
|
||||
contact = contacts.GetById(id, err);
|
||||
EXPECT_TRUE(!contact.has_value());
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Test 6: Delete Nonexistent Fails
|
||||
|
||||
```cpp
|
||||
bool Test_ContactsDeleteNotFound(std::string& error_msg) {
|
||||
mosis::ContactsInterface contacts("test.app");
|
||||
contacts.SetWritePermission(true);
|
||||
|
||||
std::string err;
|
||||
bool deleted = contacts.Delete(99999, err);
|
||||
|
||||
EXPECT_TRUE(!deleted);
|
||||
EXPECT_TRUE(err.find("not found") != std::string::npos);
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Test 7: Lua Integration
|
||||
|
||||
```cpp
|
||||
bool Test_ContactsLuaIntegration(std::string& error_msg) {
|
||||
SandboxContext ctx = TestContext();
|
||||
LuaSandbox sandbox(ctx);
|
||||
|
||||
mosis::ContactsInterface contacts("test.app");
|
||||
mosis::RegisterContactsAPI(sandbox.GetState(), &contacts);
|
||||
|
||||
std::string script = R"lua(
|
||||
-- Test that contacts global exists
|
||||
if not contacts then
|
||||
error("contacts global not found")
|
||||
end
|
||||
if not contacts.query then
|
||||
error("contacts.query not found")
|
||||
end
|
||||
if not contacts.getById then
|
||||
error("contacts.getById not found")
|
||||
end
|
||||
if not contacts.getCount then
|
||||
error("contacts.getCount not found")
|
||||
end
|
||||
if not contacts.add then
|
||||
error("contacts.add not found")
|
||||
end
|
||||
if not contacts.update then
|
||||
error("contacts.update not found")
|
||||
end
|
||||
if not contacts.delete then
|
||||
error("contacts.delete not found")
|
||||
end
|
||||
)lua";
|
||||
|
||||
bool ok = sandbox.LoadString(script, "contacts_test");
|
||||
if (!ok) {
|
||||
error_msg = "Lua test failed: " + sandbox.GetLastError();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
All tests must pass:
|
||||
|
||||
- [x] `Test_ContactsRequiresReadPermission` - Read permission required for query
|
||||
- [x] `Test_ContactsRequiresWritePermission` - Write permission required for add
|
||||
- [x] `Test_ContactsQueryWorks` - Query returns matching contacts
|
||||
- [x] `Test_ContactsQueryLimit` - Query respects limit/pagination
|
||||
- [x] `Test_ContactsCRUD` - Add/update/delete works
|
||||
- [x] `Test_ContactsDeleteNotFound` - Delete nonexistent contact fails gracefully
|
||||
- [x] `Test_ContactsLuaIntegration` - Lua API works
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Milestone 1 (LuaSandbox)
|
||||
- Milestone 2 (PermissionGate)
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
### Desktop vs Android Implementation
|
||||
|
||||
For desktop testing, ContactsInterface operates in mock mode:
|
||||
- Stores contacts in memory
|
||||
- Full CRUD support for testing
|
||||
- Search matches name, phone, or email
|
||||
|
||||
On Android, the real implementation would:
|
||||
1. Use ContentResolver to query ContactsContract
|
||||
2. Handle READ_CONTACTS and WRITE_CONTACTS permissions
|
||||
3. Use proper contact lookup/insert/update/delete URIs
|
||||
|
||||
### Security Considerations
|
||||
|
||||
1. **Separate permissions**: Read and write are independent
|
||||
2. **Limited fields**: Only name, phone, email exposed
|
||||
3. **Query limits**: Maximum 100 results per query
|
||||
4. **Audit logging**: All contact operations logged
|
||||
|
||||
### Privacy Features
|
||||
|
||||
1. **No account info**: Account type/name hidden
|
||||
2. **No photos**: Contact photos not accessible
|
||||
3. **Single values**: Only primary phone/email (no full list)
|
||||
4. **No sync metadata**: Internal sync state hidden
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
After Milestone 17 passes:
|
||||
1. Milestone 18: Inter-App Communication
|
||||
Reference in New Issue
Block a user