Android — Mobile Client + Sensor
The Android app is a full mobile client with feature parity to the web app, plus a background data collection layer that feeds the platform's context awareness.
Android is not just a sensor — it is a complete communication device. Users can make VoIP calls, send messages, view contacts and calendar, and chat with the AI, all from the native app.
Current State
The app has 20 navigation routes (defined in ui/navigation/Screen.kt) across auth, core, communications, and extended features, plus CallActivity and IncomingCallNotification which handle the in-call lifecycle outside the nav graph:
| Screen | Status |
|---|---|
| LoginScreen, SignupScreen | Active — Supabase Auth |
| ContactsScreen, ContactDetailScreen | Active — full CRUD with offline sync |
| CallsScreen | Active — Twilio Voice SDK dialer |
| InboxScreen | Active — unified inbox landing, all-channel chronological feed |
| EmailScreen | Active — threaded email inbox with folder navigation |
| ComposeEmailScreen | Active — compose, reply, forward via MS Graph |
| EmailDetailScreen | Active — full email thread with inline reply |
| MeetingScreen | Active — video meetings dashboard, scheduled + instant rooms |
| JoinMeetingScreen | Active — Twilio Video room with camera/mic/speaker controls |
| CalendarScreen | Active — monthly/weekly calendar, event detail, attendee lookup |
| ConversationsScreen | Active — SMS / messaging conversation list |
| ConversationDetailScreen | Active — SMS thread with compose |
| AiChatScreen | Active — streaming AI chat |
| SettingsScreen | Active — credentials, preferences, device info |
| IdentityScreen | Stubbed — DID/VC management |
| MarketplaceScreen | Stubbed — listing browse |
| GovernanceScreen | Stubbed — DAO/proposal view |
| TimeBankScreen | Stubbed — time credit balance |
Additional capabilities: offline sync with Room DB, FCM push notifications (5 channels), accelerometer-based activity detection, call log sync with deduplication, encrypted credential storage with EncryptedSharedPreferences, home screen widgets with live data.
What It Is
A native Kotlin app built with Jetpack Compose. It uses Twilio's Android SDKs for VoIP and video, connects directly to Supabase for data, and POST location + sensor data to the hub.
apps/android/
└── app/src/main/kotlin/io/madeopen/android/
├── ui/
│ ├── auth/ # Login / signup screens
│ ├── contacts/ # Contact list + detail
│ ├── calls/ # Dialer, CallActivity (Twilio Voice SDK)
│ ├── messaging/ # SMS conversation list + detail
│ ├── email/ # Email list, detail, compose
│ ├── video/ # Join meeting, in-meeting UI
│ ├── inbox/ # Unified inbox
│ ├── calendar/ # Calendar view
│ ├── ai/ # AI chat
│ ├── navigation/ # Screen.kt routes + AppNavigation.kt
│ └── settings/ # Credential wallet, connector config
├── data/
│ ├── repository/ # PersonRepository, ContactRepository, CredentialRepository, SecureCredentialStore
│ ├── remote/ # SupabaseClient, HubApiClient, HubApiService
│ ├── local/ # AppDatabase + Room entities/DAOs (persons, messages, conversations, inbox, email cache, outbound queue, sync queue, location cache)
│ └── sync/ # SyncManager + SyncWorker (WorkManager periodic sync)
├── sensor/
│ ├── LocationCollector.kt # FusedLocationProvider → hub
│ ├── LocationService.kt # Foreground service: continuous location streaming
│ ├── CallLogCollector.kt # Call log metadata → hub
│ ├── ActivityDetector.kt # Accelerometer-based activity detection
│ └── ContactsSyncWorker.kt # WorkManager: device contact sync
└── services/
├── LocationService.kt # Phase 11 offline-aware location service
├── TwilioVoiceService.kt # Twilio push credential + incoming call handling
├── MadeOpenFirebaseMessagingService.kt # FCM push notification router
├── SmsReplyReceiver.kt # Inline quick-reply for SMS notifications
└── FcmTokenRepository.kt # FCM token registration with hub
Tech Stack
| Layer | Technology |
|---|---|
| Language | Kotlin |
| UI | Jetpack Compose |
| Auth | Supabase Auth (Kotlin SDK) |
| Data | Supabase Kotlin SDK + Room (local cache) |
| Background sync | WorkManager |
| Voice (VoIP) | Twilio Voice Android SDK |
| Video | Twilio Video Android SDK |
| Location | FusedLocationProviderClient (Google Play Services) |
| Push notifications | Firebase Cloud Messaging (for Twilio incoming call push) |
As a Full Client
The Android app delivers the same core workflows as the web app:
| Feature | Android Implementation |
|---|---|
| Contacts | List + search (Supabase) → detail view → call / message actions |
| VoIP calls | Twilio Voice Android SDK → call screen with mute/hold/speaker |
| SMS | Thread view (Supabase) → compose + send (via hub API) |
| Video | Twilio Video Android SDK → room join, camera/mic controls |
| Calendar | Calendar view (Supabase) → event detail with attendee lookup |
| AI Chat | Chat interface → streaming responses |
| Credential Wallet | Credential list → OAuth flows → capability activation |
As a Location Sensor
The sensor layer runs in the background and streams contextual data to the hub. This is what enables location-aware rules:
"If the caller is my partner AND my current location is Work → send them an SMS with the office number instead of ringing."
Location Streaming
class LocationService : Service() {
// Foreground service with persistent notification
// FusedLocationProvider: balanced accuracy (not GPS drain)
// Batches updates: POST to hub /device-events every 60 seconds
// Respects battery: reduces frequency when device is stationary
private fun postLocationUpdate(location: Location) {
hubClient.postDeviceEvent(DeviceEvent(
type = "location.Updated",
deviceId = deviceToken,
data = LocationData(
lat = location.latitude,
lon = location.longitude,
accuracyM = location.accuracy,
wifiSsid = currentWifiSsid(),
recordedAt = Instant.now().toString()
)
))
}
}
Device Event Types
| Event | Data Collected | Used For |
|---|---|---|
location.Updated | lat, lon, accuracy, wifi SSID | Location-aware rules, location history |
sensor.WifiChanged | new SSID, signal strength | Location inference (home/work detection) |
sensor.CallLogEntry | number, duration, direction (no audio) | Sync call history to knowledge graph |
Permission Requirements
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
All permissions are requested at runtime with clear user-facing explanations. Background location requires explicit user grant (Android 10+).
Shared Design Language with Web
The Android app shares visual identity with the web app:
- Same semantic color system (primary, surface, destructive, etc.)
- Same component intent — contact cards, conversation list rows, call controls look and behave consistently
- Different implementation: Jetpack Compose Material 3 on Android; shadcn/ui + Tailwind on web
Users switching devices experience familiar layouts without relearning.
VoIP Architecture
Twilio Push Credential registered at login
│
▼
Incoming call → FCM push notification → TwilioVoiceService wakes
│
▼
CallInvite displayed as system notification (full-screen on locked screen)
│
├─ User accepts → Call screen opens → Twilio Voice SDK audio established
└─ User declines → Hub notified → voicemail or alternate routing
Outbound call:
User taps "Call" on contact
└─ App requests WebRTC token from hub
└─ Hub calls Twilio capability token endpoint
└─ Token returned to app
└─ Twilio Voice SDK places call
Offline Support
Room (local SQLite) caches the most recent data:
- Last 500 contacts
- Last 50 conversations with last 20 messages each
- Last 100 emails with headers (body fetched on demand)
- Upcoming 7 days of calendar events
WorkManager syncs changes when connectivity is restored. Conflict resolution: server wins for read data (Supabase is the source of truth); queued outbound messages are sent when online.
Offline Architecture
The offline layer has three components working together:
Room Database
data/local/
├── MadeOpenDatabase.kt # RoomDatabase — single entry point
├── dao/
│ ├── ContactDao.kt # CRUD + full-text search (FTS4)
│ ├── ConversationDao.kt # List + paginated messages
│ ├── EmailDao.kt # Headers cache, flag updates
│ └── CalendarEventDao.kt # Date-range queries
└── entity/
├── ContactEntity.kt # Mirrors persons table (subset of columns)
├── ConversationEntity.kt
├── EmailEntity.kt
└── CalendarEventEntity.kt
All Room entities map to the same shapes as the Supabase unified model. Sync is strictly one-way for read data — Supabase is the source of truth. The app never writes directly to Room for data that originates server-side; it always writes to the hub API and then refreshes from Supabase.
Outbound Message Queue
Compose actions (send email, send SMS, schedule meeting) that fail due to no connectivity are persisted in a Room OutboundQueueEntity table:
@Entity(tableName = "outbound_queue")
data class OutboundQueueEntity(
@PrimaryKey val id: String, // UUID — also the hub idempotency key
val type: String, // "email.send" | "sms.send" | "meeting.create"
val payload: String, // JSON-serialized body
val retryCount: Int = 0,
val createdAt: Long = System.currentTimeMillis()
)
WorkManager Sync
SyncWorker runs on a PeriodicWorkRequest (15-minute interval, network required). It:
- Fetches fresh contacts, conversations, emails, and calendar events from Supabase
- Upserts them into Room (replace strategy)
- Drains the outbound queue — each item is POSTed to the hub with its
idas the idempotency key - Retries failed items up to 3 times with exponential backoff; permanently failed items are surfaced as a notification
class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// 1. Sync inbound data
syncContacts()
syncConversations()
syncEmails()
syncCalendarEvents()
// 2. Drain outbound queue
drainOutboundQueue()
return Result.success()
}
}
FCM Notification Channels
MadeOpenFirebaseMessagingService routes FCM messages to channels based on the type field:
FCM type | Channel ID | Importance | Used For |
|---|---|---|---|
incoming_call | incoming_calls | MAX (full-screen) | Twilio Voice push wakeup |
new_sms | sms_messages | HIGH | SMS with inline Quick Reply via RemoteInput |
new_email | email_messages | DEFAULT | Email with sender + subject |
meeting_starting | calendar_meetings | HIGH | Calendar event with "Join now" action |
new_voicemail | voicemails | DEFAULT | Voicemail with "Play" action |
| (fallback) | made_open_commands | HIGH | Generic commands / sync alerts |
First Testable Workflow
1. Install APK on Android device
2. Open app → Sign in with Supabase Auth credentials
3. Contacts tab → verify Exchange contacts visible (synced via hub + MS Graph)
4. Tap a contact → tap "Call" → VoIP call initiated
└─ Verify: call appears in Conversations in Supabase
5. Location permission → grant "Allow all the time"
└─ Verify: location_history rows appearing in Supabase
└─ Verify: hub receives location.Updated events
6. Move to a different location
└─ Verify: rule with location condition evaluates differently
7. Receive an inbound call (have someone call your Twilio number)
└─ Verify: full-screen incoming call notification
└─ Accept → call audio works → hang up → record in Supabase
Running / Building Locally
# Open in Android Studio
# Configure local.properties with Supabase URL + anon key
# Configure google-services.json for FCM
# Or via Gradle:
cd apps/android
./gradlew assembleDebug
Requires: Android Studio, JDK 17+, a physical device or emulator with Google Play Services (for Twilio's Android SDK).