Made Open

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

  1. Navigation
  2. Screens
  3. ViewModels
  4. Services
  5. Data Layer — Room Entities and DAOs
  6. Data Layer — Remote API Clients
  7. Data Layer — Sync Architecture

Route Definitions (ui/navigation/Screen.kt)

All routes are defined as a sealed class hierarchy:

ObjectRouteParameters
Loginlogin--
Signupsignup--
Contactscontacts--
ContactDetailcontacts/{contactId}contactId: String
Callscalls--
Settingssettings--
AiChatai_chat--
Identityidentity--
Marketplacemarketplace--
Governancegovernance--
TimeBanktimebank--
Inboxinbox--
Conversationsconversations--
ConversationDetailconversations/{conversationId}conversationId: String
Emailemail--
EmailDetailemail/{emailId}emailId: String
ComposeEmailemail/compose--
JoinMeetingmeetings--
Meetingmeetings/active--
Calendarcalendar--
  • Start destination: Inbox if authenticated, Login if not (checked via supabase.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), Disconnected
  • MeetingHistoryUiState: 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 constantWhat it does
ACTION_REGISTERFetches access token from hub, gets FCM token, registers with Twilio Voice for push-based incoming calls
ACTION_PLACE_CALLFetches access token, creates ConnectOptions with To param, calls Voice.connect(), launches CallActivity
ACTION_INCOMINGReceives CallInvite parcelable, sets state to Ringing, shows IncomingCallNotification
ACTION_ACCEPTCalls callInvite.accept(), cancels notification, launches CallActivity
ACTION_REJECTCalls callInvite.reject(), cancels notification, resets to Idle
ACTION_HANGUPCalls activeCall.disconnect(), sets state to Disconnected
ACTION_TOGGLE_MUTEToggles activeCall.mute()
ACTION_TOGGLE_SPEAKERToggles 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 typeChannel IDBehaviour
incoming_callincoming_calls (MAX)Full-screen intent to lock screen call UI
new_smssms_messages (HIGH)Notification with inline Quick Reply via RemoteInput
new_emailemail_messages (DEFAULT)Notification with sender + subject
meeting_startingcalendar_meetings (HIGH)Notification with "Join now" action
new_voicemailvoicemails (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.

MethodEndpointDescription
fetchPersons()GET /contactsFetch all persons, returns List<PersonApiDto>
uploadPerson(payload)POST /contacts/sync-deviceUpload a single person record
postLocationBatch(locations)POST /api/location/batchUpload batch of location fixes
sendMessage(type, to, convId, subject, body)POST /channels/sendSend outbound SMS or email
dispatchSyncItem(entityType, operation, entityId, payload)VariousRoutes 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.

MethodEndpointDescription
postLocation(lat, lon, accuracy, timestamp)POST /api/sensor/locationSingle location fix
postCallLogEntries(entries)POST /api/sensor/call-logBatch call log upload
postActivity(type)POST /api/sensor/activityActivity state (WALKING, etc.)
postContactsSync(contacts)POST /contacts/sync-deviceFull device contact sync
postAiQuery(query, conversationId)POST /api/ai/queryAI assistant query (30s timeout)
getDid()GET /api/federation/didFetch user's DID
getCredentials()GET /api/federation/credentialsFetch verifiable credentials
generateDid()POST /api/federation/didGenerate new DID
getMarketplaceListings(query?)GET /api/marketplace/listingsSearch marketplace
getDaos()GET /api/governance/daosList user's DAOs
getProposals(daoId?)GET /api/governance/proposalsList proposals
postVote(proposalId, vote)POST /api/governance/proposals/{id}/voteCast vote
getTimeBankBalance()GET /api/timebank/balanceGet balance + transactions
postTimeBankTransfer(toUserId, amount)POST /api/timebank/transferTransfer 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()):

  1. flushOutboundQueue() -- drain offline-composed SMS/email to hub
  2. processSyncQueue() -- process queued entity changes (person, rule)
  3. syncLocationCache() -- batch-upload buffered location fixes
  4. syncCallLog() -- read new call log entries and upload
  5. syncDown() -- 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.