Android App Reference
Complete technical reference for the Made Open Android client. Every screen, ViewModel, service, Room entity, DAO, and API client method documented from source.
Package root: io.madeopen.android
Min SDK: Android 8.0+ (API 26); targetSdk 35, compileSdk 35
UI toolkit: Jetpack Compose + Material 3
Architecture: MVVM with StateFlow, offline-first via Room + SyncManager
Table of Contents
- Navigation
- Screens
- ViewModels
- Services
- Data Layer — Room Entities and DAOs
- Data Layer — Remote API Clients
- Data Layer — Sync Architecture
Navigation
Route Definitions (ui/navigation/Screen.kt)
All routes are defined as a sealed class hierarchy:
| Object | Route | Parameters |
|---|---|---|
Login | login | -- |
Signup | signup | -- |
Contacts | contacts | -- |
ContactDetail | contacts/{contactId} | contactId: String |
Calls | calls | -- |
Settings | settings | -- |
AiChat | ai_chat | -- |
Identity | identity | -- |
Marketplace | marketplace | -- |
Governance | governance | -- |
TimeBank | timebank | -- |
Inbox | inbox | -- |
Conversations | conversations | -- |
ConversationDetail | conversations/{conversationId} | conversationId: String |
Email | email | -- |
EmailDetail | email/{emailId} | emailId: String |
ComposeEmail | email/compose | -- |
JoinMeeting | meetings | -- |
Meeting | meetings/active | -- |
Calendar | calendar | -- |
Navigation Graph (ui/navigation/AppNavigation.kt)
- Start destination:
Inboxif authenticated,Loginif not (checked viasupabase.auth.currentSessionOrNull()). - Auth flow: Login success and Signup success both navigate to Inbox and clear the back stack.
- Sign out: Settings screen calls
supabase.auth.signOut()then navigates to Login clearing entire back stack. - Inbox routing: Tapping an inbox item navigates based on
item.type:call-> Calls,sms-> Conversations,email-> EmailDetail or Email,video-> JoinMeeting,calendar_event-> Calendar. - Meeting flow: JoinMeeting -> Meeting (on connect); leaving a meeting navigates back to JoinMeeting.
Screens
LoginScreen (ui/auth/LoginScreen.kt)
Displays: Made Open branding, email + password fields, sign-in button, link to signup.
ViewModel: AuthViewModel
User actions: Enter email/password, toggle password visibility, submit login, navigate to signup.
API calls: supabase.auth.signInWith(Email) via AuthViewModel.
Behaviour: Shows CircularProgressIndicator during loading; snackbar on error; navigates to Inbox on success.
SignupScreen (ui/auth/SignupScreen.kt)
Displays: Made Open branding ("Own your data"), email + password + confirm password fields, create account button, link to login.
ViewModel: AuthViewModel
User actions: Enter email/password/confirm, toggle password visibility, submit signup.
API calls: supabase.auth.signUpWith(Email) via AuthViewModel.
Validation: Password minimum 6 characters; confirm password must match (inline error).
InboxScreen (ui/inbox/InboxScreen.kt)
Displays: Unified feed of all communication types (calls, SMS, email, video, voicemail, calendar events) in a single scrollable list. Horizontal filter chips (All, Calls, SMS, Email, Video, Voicemail). Unread badge in title bar. Bold text + dot indicator for unread items.
ViewModel: InboxViewModel (AndroidViewModel)
User actions: Filter by type, pull-to-refresh, tap item (marks read then navigates by type), navigate to settings.
API calls: GET /api/inbox?limit=50&type=... via HttpURLConnection; PUT /api/inbox/read to mark items read on hub.
Offline: Observes Room InboxItemDao for instant display; remote fetch upserts into DB cache.
ContactsScreen (ui/contacts/ContactsScreen.kt)
Displays: Searchable contact list with cards showing name, email, phone. Top bar actions: location toggle, AI assistant, calls, settings.
ViewModel: ContactsViewModel
User actions: Search contacts (debounced 300ms), tap contact for detail, toggle location service, navigate to AI/calls/settings.
API calls: Contacts loaded via ContactRepository (Supabase Postgrest).
Location: Requests ACCESS_FINE_LOCATION + ACCESS_COARSE_LOCATION; starts/stops LocationService as a foreground service.
ContactDetailScreen (ui/contacts/ContactDetailScreen.kt)
Displays: Two-tab layout: Info tab (header card with avatar initial, name, source system, email section, phone section, tags as chips) and Timeline tab (chronological communication history across all channels).
ViewModel: ContactsViewModel (shared with ContactsScreen)
User actions: Switch between Info/Timeline tabs, navigate back.
API calls: ContactRepository.getContactById() for detail; Supabase Postgrest messages table (filtered by owner, ordered by created_at desc, limit 50) for timeline.
Timeline icons: voice -> Call, sms -> Message, email -> Email, video -> VideoCall, calendar -> Event.
CallsScreen (ui/calls/CallsScreen.kt)
Displays: Phone number input field, T9 dial pad (1-9, *, 0, #), call button, recent calls list. Warning text if Twilio not configured.
ViewModel: CallsViewModel
User actions: Dial digits, delete, enter number, place call, navigate to contacts/settings.
API calls: Queries Supabase messages table where channel_type = "voice" for recent calls; checks CredentialRepository for Twilio configuration status.
Call placement: Starts TwilioVoiceService via intent with ACTION_PLACE_CALL.
CallActivity (ui/calls/CallActivity.kt)
Displays: Full-screen in-call UI with caller name/number, call duration timer (MM:SS), call state text ("Incoming call...", "Connecting...", timer). For incoming: Accept (green) + Decline (red) buttons. For active: Mute, Speaker, Keypad controls + hang up button.
ViewModel: None (standalone ComponentActivity)
User actions: Accept/reject incoming call, mute toggle, speaker toggle, hang up.
Communication: All controls delegate to TwilioVoiceService via service intents.
IncomingCallNotification (ui/calls/IncomingCallNotification.kt)
Displays: High-importance notification with full-screen intent for lock screen. Shows "Incoming Call" + caller name. Accept/Decline actions.
Channel: made_open_incoming_call (IMPORTANCE_HIGH).
Actions: Accept -> TwilioVoiceService.ACTION_ACCEPT; Decline -> TwilioVoiceService.ACTION_REJECT.
Full-screen: Opens CallActivity with EXTRA_IS_INCOMING = true.
ConversationsScreen (ui/messaging/ConversationsScreen.kt)
Displays: SMS/MMS conversation thread list with avatar placeholder, last message snippet, timestamp, unread badge count. Pull-to-refresh.
ViewModel: ConversationsViewModel (AndroidViewModel)
User actions: Pull to refresh, tap conversation to open detail.
API calls: GET /api/conversations?limit=50 via HttpURLConnection.
Offline: Observes Room ConversationDao.getAll().
ConversationDetailScreen (ui/messaging/ConversationDetailScreen.kt)
Displays: Message thread with chat bubbles (outbound = primary color, right-aligned; inbound = surface variant, left-aligned). Compose row with text field + send button. Auto-scrolls to bottom on new messages.
ViewModel: ConversationDetailViewModel (with factory for conversationId)
User actions: Type message, send message.
API calls: GET /api/conversations/{id}/messages?limit=100 via HttpURLConnection.
Send flow: Optimistic local insert into Room (MessageEntity with isDirty = true, fromAddress = "me"), then enqueues OutboundMessageEntity for hub delivery via SyncManager.
Offline: Observes Room MessageDao.getForConversation().
EmailScreen (ui/email/EmailScreen.kt)
Displays: Email inbox with navigation drawer for folders (Inbox, Sent, Drafts, Archive, Trash). Email list shows unread indicator dot, sender, subject, body preview, date, attachment icon. Compose FAB.
ViewModel: EmailViewModel
User actions: Open folder drawer, select folder, tap email for detail, compose new email, refresh.
API calls: Supabase Postgrest messages table where channel_type = "email", ordered by created_at desc, limit 50.
EmailDetailScreen (ui/email/EmailDetailScreen.kt)
Displays: Email thread view with expandable/collapsible message cards. Subject header, sender, date, To/CC metadata, full body text. Animated expand/collapse with AnimatedVisibility. Reply + Forward actions in top bar.
ViewModel: EmailViewModel (shared, uses detailState)
User actions: Expand/collapse thread messages, reply, forward, navigate back.
API calls: EmailViewModel.loadEmailDetail() queries Supabase messages table by id.
ComposeEmailScreen (ui/email/ComposeEmailScreen.kt)
Displays: Email composition form with To, CC (expandable), BCC (expandable), Subject, Body fields. Attach + Send actions in top bar. Error dialog on send failure.
ViewModel: EmailViewModel (shared, uses compose state flows)
User actions: Fill To/CC/BCC/Subject/Body, toggle CC/BCC visibility, attach file (placeholder), send email, discard.
API calls: EmailViewModel.sendEmail() -- currently records intent; hub /api/email/send integration pending.
JoinMeetingScreen (ui/video/JoinMeetingScreen.kt)
Displays: Video call icon, room name text field, join button, Twilio configuration warning, recent meetings list with "Rejoin" action.
ViewModel: VideoViewModel
User actions: Enter room name, join meeting, rejoin past meeting, navigate to settings.
API calls: Supabase Postgrest messages table where channel_type = "video" for meeting history. Checks CredentialRepository for Twilio status.
Navigation: Auto-navigates to MeetingScreen when meetingState transitions to Connected.
MeetingScreen (ui/video/MeetingScreen.kt)
Displays: Full-screen video meeting UI with black background. Room name bar with participant count. Participant grid (adaptive 1 or 2 columns) with camera-off avatars or camera preview placeholders. Identity labels + mute indicators. Bottom toolbar: mic toggle, camera toggle, leave (red).
ViewModel: VideoViewModel (shared)
User actions: Toggle mute, toggle camera, leave meeting.
States: Connecting (spinner), Connected (grid + controls), Error (message), Disconnected (auto-navigates away).
CalendarScreen (ui/calendar/CalendarScreen.kt)
Displays: Full calendar with three view modes (Month, Week, Day). Month grid with day-of-week headers, today highlight, selected date highlight, event dot indicators. Week row with day name + date. Event list for selected date showing time, color bar, title, location, organizer. Navigation arrows + "Today" button.
ViewModel: CalendarViewModel
User actions: Select date, switch view mode (Month/Week/Day), navigate previous/next period, jump to today, refresh.
API calls: Supabase Postgrest messages table where channel_type = "calendar", ordered ascending, limit 200.
SettingsScreen (ui/settings/SettingsScreen.kt)
Displays: Three card sections: Account (email + sign out), Twilio Credentials (Account SID, Auth Token, Phone Number), Microsoft 365 Credentials (Client ID, Client Secret, Tenant ID). Secret fields with visibility toggle.
ViewModel: SettingsViewModel
User actions: Sign out, edit/save Twilio credentials, edit/save MS Graph credentials.
API calls: CredentialRepository.loadCredentials() and CredentialRepository.saveCredentials() for both credential sets.
Events: Snackbar messages for save success/failure via SettingsUiEvent.ShowMessage.
AiChatScreen (ui/ai/AiChatScreen.kt)
Displays: Chat interface with message bubbles (user = primary/right, assistant = surface variant/left). Text input + send button. Voice input button (if speech recognition available). Loading spinner during AI response. Empty state: "Ask me anything about your contacts, calendar, messages, or tasks."
ViewModel: AiChatViewModel (with ViewModelProvider.Factory)
User actions: Type message, send, voice input (Android SpeechRecognizer), navigate back.
API calls: POST /api/ai/query via HubApiClient with { text, conversationId }. Returns { answer, conversationId }.
Conversation: Maintains conversationId across messages for context continuity.
IdentityScreen (ui/identity/IdentityScreen.kt)
Displays: Two-tab layout: "My Identity" (DID card with copy button, public key card with copy button, verifiable credentials list) and "Social Graph" (follower/following counts, recent connections list with DID, status badges: accepted/pending).
ViewModel: IdentityViewModel
User actions: Generate DID, copy DID to clipboard, copy public key, switch tabs.
API calls: GET /api/federation/did, GET /api/federation/credentials, POST /api/federation/did (generate) via HubApiClient.
MarketplaceScreen (ui/marketplace/MarketplaceScreen.kt)
Displays: Search bar, horizontal filter chips (All, Data, Services, Physical, Digital), listing cards with title, type badge (color-coded), status badge, price. Create listing FAB.
ViewModel: MarketplaceViewModel
User actions: Search (debounced 300ms), filter by type, create listing (placeholder).
API calls: GET /api/marketplace/listings?q=... via HubApiClient.
Local filter: Client-side filtering by type after server search.
GovernanceScreen (ui/governance/GovernanceScreen.kt)
Displays: Two-tab layout: "My DAOs" (DAO cards with name, role badge Admin/Member, member count, open proposals count) and "Proposals" (proposal cards with title, DAO name, vote counts for Yes/No/Abstain, voted indicator, vote buttons).
ViewModel: GovernanceViewModel
User actions: Switch tabs, vote on proposals (Yes/No/Abstain).
API calls: GET /api/governance/daos, GET /api/governance/proposals, POST /api/governance/proposals/{id}/vote via HubApiClient.
Optimistic update: Vote counts update locally before server confirmation.
TimeBankScreen (ui/timebank/TimeBankScreen.kt)
Displays: Balance card (large credit count, lifetime earned/spent), recent transactions list (icon + color for earned/spent/transfer), transfer FAB. Modal bottom sheet for transfers with recipient ID, amount, validation.
ViewModel: TimeBankViewModel
User actions: View balance/history, initiate transfer (FAB), fill recipient + amount, submit transfer.
API calls: GET /api/timebank/balance (returns balance, lifetimeEarned, lifetimeSpent, transactions), POST /api/timebank/transfer via HubApiClient.
Validation: Recipient required, positive amount, sufficient balance.
Optimistic update: Balance and transaction list update locally on successful transfer.
ViewModels
AuthViewModel (ui/auth/AuthViewModel.kt)
State: AuthState sealed class: Idle, Loading, Success, Error(message).
Key methods: login(email, password), signUp(email, password), resetState().
Data source: Supabase Auth (signInWith(Email), signUpWith(Email)).
ContactsViewModel (ui/contacts/ContactsViewModel.kt)
State shapes:
ContactsUiState:Loading,Success(contacts),Error(message)ContactDetailUiState:Loading,Success(contact),Error(message)ContactTimelineUiState:Idle,Loading,Success(events),Error(message)
Key methods: loadContacts(), onSearchQueryChange(query), loadContactDetail(contactId), loadContactTimeline(contactId).
Data sources: ContactRepository for contacts; Supabase Postgrest messages table for timeline.
Search: Debounced 300ms via MutableStateFlow + debounce().
CallsViewModel (ui/calls/CallsViewModel.kt)
State: CallsUiState: Loading, Success(recentCalls, twilioConfigured), Error(message).
Key methods: loadCallsData(), onDialPadPress(digit), onDialPadClear(), onDialPadDelete(), onPhoneNumberChange(number), placeCall(context, to).
Data sources: Supabase Postgrest messages where channel_type = "voice"; CredentialRepository for Twilio status.
Call: Starts TwilioVoiceService via context.startForegroundService().
InboxViewModel (ui/inbox/InboxViewModel.kt)
State: InboxUiState: Loading, Success(items, unreadCount, isRefreshing), Error(message).
Key methods: setFilter(filter), refresh(), markRead(ids), markStarred(ids, starred), archive(ids).
Data sources: Room InboxItemDao (observed via Flow + flatMapLatest for filter changes + combine with unread count); Hub GET /api/inbox.
Offline-first: DB observer drives UI; remote fetch upserts into cache.
ConversationsViewModel (ui/messaging/ConversationsViewModel.kt)
State: ConversationsUiState: Loading, Success(conversations, isRefreshing), Error(message).
Key methods: refresh().
Data sources: Room ConversationDao.getAll() (observed); Hub GET /api/conversations?limit=50.
ConversationDetailViewModel (ui/messaging/ConversationDetailViewModel.kt)
State: ConversationDetailUiState: Loading, Success(messages), Error(message).
Key methods: setDraft(text), sendMessage().
Data sources: Room MessageDao.getForConversation() (observed); Hub GET /api/conversations/{id}/messages?limit=100.
Send: Optimistic local insert + enqueue OutboundMessageEntity for offline-safe delivery.
Factory: ConversationDetailViewModelFactory(conversationId).
EmailViewModel (ui/email/EmailViewModel.kt)
State shapes:
EmailUiState:Loading,Success(emails, folders, selectedFolder),Error(message)EmailDetailUiState:Idle,Loading,Success(email, thread),Error(message)ComposeEmailUiState:Idle,Sending,Sent,Error(message)
Key methods: loadEmails(folder), selectFolder(folder), loadEmailDetail(emailId), sendEmail(), onComposeTo/Cc/Bcc/Subject/BodyChange(), resetComposeState().
Data source: Supabase Postgrest messages where channel_type = "email".
Compose fields: Separate StateFlows for composeTo, composeCc, composeBcc, composeSubject, composeBody.
VideoViewModel (ui/video/VideoViewModel.kt)
State shapes:
JoinMeetingUiState:Idle,Joining,Error(message)MeetingUiState:Connecting,Connected(roomName, participants, isMuted, isCameraOff, twilioConfigured),Error(message),DisconnectedMeetingHistoryUiState:Loading,Success(meetings, twilioConfigured),Error(message)
Key methods: onRoomNameChange(value), joinMeeting(roomName), toggleMute(), toggleCamera(), leaveMeeting(), resetJoinError().
Data sources: Supabase Postgrest messages where channel_type = "video" for history; CredentialRepository for Twilio status.
Participant model: VideoParticipant(identity, isMuted, isCameraOff, isLocal).
CalendarViewModel (ui/calendar/CalendarViewModel.kt)
State: CalendarUiState: Loading, Success(events, selectedDate, viewMode, currentMonth, eventsForSelectedDate), Error(message).
Key methods: loadEvents(), selectDate(date), setViewMode(mode), previousPeriod(), nextPeriod(), goToToday().
View modes: CalendarViewMode.MONTH, WEEK, DAY -- period navigation adjusts by month/week/day respectively.
Data source: Supabase Postgrest messages where channel_type = "calendar", ascending, limit 200.
Event filtering: Client-side filtering by metadata.startTime ISO date matching selected date.
SettingsViewModel (ui/settings/SettingsViewModel.kt)
State: SettingsUiState data class with fields: isLoading, userEmail, twilioAccountSid, twilioAuthToken, twilioPhoneNumber, msClientId, msClientSecret, msTenantId, isSavingTwilio, isSavingMsGraph.
Events: SettingsUiEvent.ShowMessage(message) for snackbar feedback.
Key methods: loadSettings(), onTwilio*Change(), onMs*Change(), saveTwilioCredentials(), saveMsGraphCredentials(), clearEvent().
Data source: CredentialRepository for load/save; supabase.auth.currentUserOrNull() for email.
AiChatViewModel (ui/ai/AiChatViewModel.kt)
State: AiChatUiState(messages, isLoading, conversationId, error).
Key methods: sendMessage(query), clearError().
Data source: HubApiClient.postAiQuery() via injectable AiQueryFn for testability.
Conversation: Maintains conversationId across messages for multi-turn context.
Factory: AiChatViewModel.Factory companion object for default dependency injection.
IdentityViewModel (ui/identity/IdentityViewModel.kt)
State: IdentityUiState(did, publicKey, followers, following, credentials, recentFollows, isLoading, error).
Key methods: loadIdentity(), generateDid().
Data source: HubApiClient.getDid(), HubApiClient.getCredentials(), HubApiClient.generateDid().
Models: CredentialItem(type, issuer, issuedAt), PeerItem(id, did, displayName, status).
MarketplaceViewModel (ui/marketplace/MarketplaceViewModel.kt)
State: MarketplaceUiState(listings, query, filter, isLoading, error).
Key methods: onQueryChange(query), onFilterChange(filter), loadListings(query?).
Data source: HubApiClient.getMarketplaceListings(query, token).
Search: Debounced 300ms. Local post-filter by type (Data/Services/Physical/Digital).
GovernanceViewModel (ui/governance/GovernanceViewModel.kt)
State: GovernanceUiState(daos, proposals, isLoading, error).
Key methods: loadGovernance(), vote(proposalId, vote).
Data source: HubApiClient.getDaos(), HubApiClient.getProposals(), HubApiClient.postVote().
Optimistic update: On successful vote, proposal's hasVoted, userVote, and vote count fields update locally.
TimeBankViewModel (ui/timebank/TimeBankViewModel.kt)
State: TimeBankUiState(balance, lifetimeEarned, lifetimeSpent, transactions, isTransferSheetOpen, transferToUserId, transferAmount, transferError, isLoading, error).
Key methods: loadBalance(), openTransferSheet(), closeTransferSheet(), onTransferUserIdChange(), onTransferAmountChange(), submitTransfer().
Data source: HubApiClient.getTimeBankBalance(), HubApiClient.postTimeBankTransfer().
Validation: Recipient required, positive amount, balance check.
Services
TwilioVoiceService (services/TwilioVoiceService.kt)
Foreground service managing the Twilio Voice SDK call lifecycle.
Call state machine: Idle -> Ringing(from, callInvite) -> Connected(call) -> Disconnected -> Idle.
Actions (intent-based):
| Action constant | What it does |
|---|---|
ACTION_REGISTER | Fetches access token from hub, gets FCM token, registers with Twilio Voice for push-based incoming calls |
ACTION_PLACE_CALL | Fetches access token, creates ConnectOptions with To param, calls Voice.connect(), launches CallActivity |
ACTION_INCOMING | Receives CallInvite parcelable, sets state to Ringing, shows IncomingCallNotification |
ACTION_ACCEPT | Calls callInvite.accept(), cancels notification, launches CallActivity |
ACTION_REJECT | Calls callInvite.reject(), cancels notification, resets to Idle |
ACTION_HANGUP | Calls activeCall.disconnect(), sets state to Disconnected |
ACTION_TOGGLE_MUTE | Toggles activeCall.mute() |
ACTION_TOGGLE_SPEAKER | Toggles AudioManager.isSpeakerphoneOn |
Access token: Fetched from GET {HUB_URL}/api/calling/token with Bearer auth.
Notification channel: made_open_voice (IMPORTANCE_LOW) for foreground service.
Lifecycle: CoroutineScope(SupervisorJob() + Dispatchers.IO) cancelled on onDestroy().
MadeOpenFirebaseMessagingService (services/MadeOpenFirebaseMessagingService.kt)
FCM messaging service handling push notifications for all communication types.
Message type routing:
FCM type | Channel ID | Behaviour |
|---|---|---|
incoming_call | incoming_calls (MAX) | Full-screen intent to lock screen call UI |
new_sms | sms_messages (HIGH) | Notification with inline Quick Reply via RemoteInput |
new_email | email_messages (DEFAULT) | Notification with sender + subject |
meeting_starting | calendar_meetings (HIGH) | Notification with "Join now" action |
new_voicemail | voicemails (DEFAULT) | Notification with "Play" action |
| (fallback) | made_open_commands (HIGH) | Generic notification |
Token refresh: On onNewToken(), registers with hub via FcmTokenRepository.registerToken().
Quick Reply: SMS notifications include RemoteInput action; reply captured by SmsReplyReceiver.
Intent extras: screen, call_sid, conv_id, sender, email_id, event_id, join_url, voicemail_id, action.
SmsReplyReceiver (services/SmsReplyReceiver.kt)
BroadcastReceiver for inline Quick Reply actions on new-SMS notifications.
Flow: Extracts reply text from RemoteInput, persists as OutboundMessageEntity in Room, dismisses notification.
Offline-safe: Message enqueued locally; SyncManager.flushOutboundQueue() delivers on next sync cycle.
Data Layer -- Room Entities and DAOs
AppDatabase (data/local/AppDatabase.kt)
Room database made_open.db, version 2, with fallbackToDestructiveMigration().
Entities: PersonEntity, MessageEntity, SyncQueueEntity, LocationCacheEntity, InboxItemEntity, ConversationEntity, EmailCacheEntity, OutboundMessageEntity.
DAOs: personDao, messageDao, syncQueueDao, locationCacheDao, inboxItemDao, conversationDao, emailCacheDao, outboundMessageDao.
Singleton: Thread-safe double-checked locking via companion object.
Converters (data/local/Converters.kt)
Room TypeConverters for List<String> <-> JSON array string (using org.json.JSONArray).
PersonEntity / PersonDao
Table: persons
Fields: id (PK), name, emails (JSON array), phones (JSON array), tags (JSON array), sourceId, updatedAt, syncedAt, isDirty.
DAO methods: getAll() (Flow, alpha order), getById(id), upsert(person), getDirty(), markSynced(id, time).
MessageEntity / MessageDao
Table: messages
Fields: id (PK), conversationId, fromAddress, subject, body, receivedAt, syncedAt, isDirty.
DAO methods: getForConversation(convId) (Flow, newest first), upsert(msg), getDirty().
InboxItemEntity / InboxItemDao
Table: inbox_items
Fields: id (PK), type (call/sms/email/video/calendar_event/voicemail), conversationId, contactId, contactName, channelType, direction, subject, preview, timestamp, read, starred, archived, syncedAt.
DAO methods: getInbox(limit, offset) (Flow, non-archived, newest first), getByType(type), getUnread(), getUnreadCount() (Flow), getUnreadCountByType(type), getByContact(contactId), upsert(item), upsertAll(items), markRead(ids), markStarred(ids, starred), archive(ids), delete(id), clear().
ConversationEntity / ConversationDao
Table: conversations
Fields: id (PK), participantAddresses (JSON array), lastMessageSnippet, lastMessageAt, unreadCount, syncedAt.
DAO methods: getAll() (Flow, most recent first), getById(id), upsert(conversation), upsertAll(conversations), decrementUnread(id, by), clear().
EmailCacheEntity / EmailCacheDao
Table: email_cache
Fields: id (PK), fromAddress, fromName, toAddresses (JSON array), ccAddresses (JSON array), subject, bodyHtml, snippet, receivedAt, isRead, hasAttachments, syncedAt.
DAO methods: getAll() (Flow, newest first), getUnread(), getById(id), upsert(email), upsertAll(emails), markRead(id), pruneOld(cutoffMs), clear().
OutboundMessageEntity / OutboundMessageDao
Table: outbound_messages
Fields: id (PK, auto), type (sms/email), toAddress, convId, subject, body, createdAt, attempts, nextRetryAt, isSent.
DAO methods: enqueue(message), getDue(now), observePending() (Flow), recordAttempt(id, nextRetryAt), markSent(id), delete(message), pruneSent(cutoffMs), clear().
SyncQueueEntity / SyncQueueDao
Table: sync_queue
Fields: id (PK, auto), entityType (person/message/rule), entityId, operation (create/update/delete), payload (JSON), attempts, createdAt, nextRetryAt.
DAO methods: enqueue(item), getDue(now) (limit 50, ordered by createdAt), delete(item), recordAttempt(id, nextRetry).
LocationCacheEntity / LocationCacheDao
Table: location_cache
Fields: id (PK, auto), lat, lng, accuracy, altitude, speed, bearing, capturedAt, synced.
DAO methods: insert(location), getUnsynced() (limit 100, oldest first), markSynced(ids), pruneOld(cutoff).
Data Layer -- Remote API Clients
HubApiService (data/remote/HubApiService.kt)
Typed HTTP service for sync-related endpoints. Uses HttpURLConnection.
| Method | Endpoint | Description |
|---|---|---|
fetchPersons() | GET /contacts | Fetch all persons, returns List<PersonApiDto> |
uploadPerson(payload) | POST /contacts/sync-device | Upload a single person record |
postLocationBatch(locations) | POST /api/location/batch | Upload batch of location fixes |
sendMessage(type, to, convId, subject, body) | POST /channels/send | Send outbound SMS or email |
dispatchSyncItem(entityType, operation, entityId, payload) | Various | Routes to /contacts/sync-device or /rules |
DTOs: PersonApiDto(id, name, emails, phones, tags, sourceId, updatedAt), LocationBatchItem(lat, lng, accuracy, altitude, speed, bearing, capturedAt).
Extension: PersonApiDto.toEntity() converts to PersonEntity.
HubApiClient (data/remote/HubApiClient.kt)
Singleton HTTP client for feature-specific hub endpoints. Uses HttpURLConnection.
| Method | Endpoint | Description |
|---|---|---|
postLocation(lat, lon, accuracy, timestamp) | POST /api/sensor/location | Single location fix |
postCallLogEntries(entries) | POST /api/sensor/call-log | Batch call log upload |
postActivity(type) | POST /api/sensor/activity | Activity state (WALKING, etc.) |
postContactsSync(contacts) | POST /contacts/sync-device | Full device contact sync |
postAiQuery(query, conversationId) | POST /api/ai/query | AI assistant query (30s timeout) |
getDid() | GET /api/federation/did | Fetch user's DID |
getCredentials() | GET /api/federation/credentials | Fetch verifiable credentials |
generateDid() | POST /api/federation/did | Generate new DID |
getMarketplaceListings(query?) | GET /api/marketplace/listings | Search marketplace |
getDaos() | GET /api/governance/daos | List user's DAOs |
getProposals(daoId?) | GET /api/governance/proposals | List proposals |
postVote(proposalId, vote) | POST /api/governance/proposals/{id}/vote | Cast vote |
getTimeBankBalance() | GET /api/timebank/balance | Get balance + transactions |
postTimeBankTransfer(toUserId, amount) | POST /api/timebank/transfer | Transfer credits |
Data Layer -- Sync Architecture
SyncManager (data/sync/SyncManager.kt)
Orchestrates two-way sync between Room and the hub.
Dependencies: PersonDao, MessageDao, SyncQueueDao, LocationCacheDao, OutboundMessageDao, HubApiService, ConnectivityManager, CallLogCollector, auth token function, SharedPreferences.
Full sync flow (sync()):
flushOutboundQueue()-- drain offline-composed SMS/email to hubprocessSyncQueue()-- process queued entity changes (person, rule)syncLocationCache()-- batch-upload buffered location fixessyncCallLog()-- read new call log entries and uploadsyncDown()-- fetch persons from hub and upsert into Room
Back-off strategy: Exponential, base 30s, max 4 shifts (caps at ~8 minutes). Applied to both sync queue and outbound message queue.
Location pruning: Synced records older than 7 days are deleted.
Outbound pruning: Sent messages older than 24 hours are deleted.
Call log dedup: Uses high-water mark timestamp stored in SharedPreferences (call_log_last_sync_ms).
Connectivity check: isOnline() via ConnectivityManager.activeNetwork + NET_CAPABILITY_INTERNET.
SyncWorker (data/sync/SyncWorker.kt)
WorkManager CoroutineWorker that runs SyncManager.sync() every 15 minutes.
Constraints: Requires NetworkType.CONNECTED.
Policy: ExistingPeriodicWorkPolicy.KEEP prevents duplicate instances.
Retry: Up to 3 attempts on failure before reporting Result.failure().
Work name: periodic_sync.