Files
MosisService/docs/SANDBOX_MILESTONE_17.md

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.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

// 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

-- 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 query
  • Test_ContactsRequiresWritePermission - Write permission required for add
  • Test_ContactsQueryWorks - Query returns matching contacts
  • Test_ContactsQueryLimit - Query respects limit/pagination
  • Test_ContactsCRUD - Add/update/delete works
  • Test_ContactsDeleteNotFound - Delete nonexistent contact fails gracefully
  • 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