Made Open

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:

ScreenStatus
LoginScreen, SignupScreenActive — Supabase Auth
ContactsScreen, ContactDetailScreenActive — full CRUD with offline sync
CallsScreenActive — Twilio Voice SDK dialer
InboxScreenActive — unified inbox landing, all-channel chronological feed
EmailScreenActive — threaded email inbox with folder navigation
ComposeEmailScreenActive — compose, reply, forward via MS Graph
EmailDetailScreenActive — full email thread with inline reply
MeetingScreenActive — video meetings dashboard, scheduled + instant rooms
JoinMeetingScreenActive — Twilio Video room with camera/mic/speaker controls
CalendarScreenActive — monthly/weekly calendar, event detail, attendee lookup
ConversationsScreenActive — SMS / messaging conversation list
ConversationDetailScreenActive — SMS thread with compose
AiChatScreenActive — streaming AI chat
SettingsScreenActive — credentials, preferences, device info
IdentityScreenStubbed — DID/VC management
MarketplaceScreenStubbed — listing browse
GovernanceScreenStubbed — DAO/proposal view
TimeBankScreenStubbed — 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

LayerTechnology
LanguageKotlin
UIJetpack Compose
AuthSupabase Auth (Kotlin SDK)
DataSupabase Kotlin SDK + Room (local cache)
Background syncWorkManager
Voice (VoIP)Twilio Voice Android SDK
VideoTwilio Video Android SDK
LocationFusedLocationProviderClient (Google Play Services)
Push notificationsFirebase Cloud Messaging (for Twilio incoming call push)

As a Full Client

The Android app delivers the same core workflows as the web app:

FeatureAndroid Implementation
ContactsList + search (Supabase) → detail view → call / message actions
VoIP callsTwilio Voice Android SDK → call screen with mute/hold/speaker
SMSThread view (Supabase) → compose + send (via hub API)
VideoTwilio Video Android SDK → room join, camera/mic controls
CalendarCalendar view (Supabase) → event detail with attendee lookup
AI ChatChat interface → streaming responses
Credential WalletCredential 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

EventData CollectedUsed For
location.Updatedlat, lon, accuracy, wifi SSIDLocation-aware rules, location history
sensor.WifiChangednew SSID, signal strengthLocation inference (home/work detection)
sensor.CallLogEntrynumber, 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:

  1. Fetches fresh contacts, conversations, emails, and calendar events from Supabase
  2. Upserts them into Room (replace strategy)
  3. Drains the outbound queue — each item is POSTed to the hub with its id as the idempotency key
  4. 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 typeChannel IDImportanceUsed For
incoming_callincoming_callsMAX (full-screen)Twilio Voice push wakeup
new_smssms_messagesHIGHSMS with inline Quick Reply via RemoteInput
new_emailemail_messagesDEFAULTEmail with sender + subject
meeting_startingcalendar_meetingsHIGHCalendar event with "Join now" action
new_voicemailvoicemailsDEFAULTVoicemail with "Play" action
(fallback)made_open_commandsHIGHGeneric 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).