# 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 #include #include #include #include #include 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 Query(const QueryOptions& options, std::string& error); // Get single contact by ID (requires read permission) std::optional 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& 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 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 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 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