12 KiB
12 KiB
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.readandcontacts.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
- ContactsInterface class - Contact query and management
- Contact struct - Limited contact fields (privacy-preserving)
- Lua contacts API -
contacts.query(),contacts.add(),contacts.update(),contacts.delete() - 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
// 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 identifiername- Display namephone- Primary phone numberemail- 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
-- 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
-- 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
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
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
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
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
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
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
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:
Test_ContactsRequiresReadPermission- Read permission required for queryTest_ContactsRequiresWritePermission- Write permission required for addTest_ContactsQueryWorks- Query returns matching contactsTest_ContactsQueryLimit- Query respects limit/paginationTest_ContactsCRUD- Add/update/delete worksTest_ContactsDeleteNotFound- Delete nonexistent contact fails gracefullyTest_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:
- Use ContentResolver to query ContactsContract
- Handle READ_CONTACTS and WRITE_CONTACTS permissions
- Use proper contact lookup/insert/update/delete URIs
Security Considerations
- Separate permissions: Read and write are independent
- Limited fields: Only name, phone, email exposed
- Query limits: Maximum 100 results per query
- Audit logging: All contact operations logged
Privacy Features
- No account info: Account type/name hidden
- No photos: Contact photos not accessible
- Single values: Only primary phone/email (no full list)
- No sync metadata: Internal sync state hidden
Next Steps
After Milestone 17 passes:
- Milestone 18: Inter-App Communication