Skip to content

Android App — Status, Architecture & Continuation Guide

Living document — update after every working session. READ THIS FIRST. Last updated: 2026-07-15 (session: full Samsung One UI redesign — app-wide theme + OneUiCard/OneUiHeader kit, every screen swept + the 4 stock screens facelifted + a motion pass; nav 5→4 tabs (Recents+Keypad merged into Phone = recents list + dialpad bottom sheet); call-info + contact + ticket detail pages; Contacts Google-style cards; iOS analytics parity (Pulse fitness-tile layout); Recents own-extension scoping + To/From; professional Settings; single OS splash. Call bugs fixed on the V2037: real-device one-way audio (RECORD_AUDIO at call-time), incoming ringtone (self-managed rings itself) + 603-Decline + wake-race, proximity screen-off — see Errors 97–100 in troubleshooting.md. Release-signed (Play App Signing; upload key in gitignored keystore.properties); security audit closed 2 real exported-IPC vulns (IncomingCallActivity/WakeReceiver → exported=false) + allowBackup=false + server allowlist + release log gating. AAB 0.4.4 [10] built, uploaded to Internal testing. Play listing kit: android-play-listing.md. Prior (07-09): engine cutover F1+F2, prod WSS listener.)

Android is phase 2; iOS (status) is phase 1. The master rebuild plan is ~/.claude/plans/glistening-imagining-clarke.md; the screen-by-screen iOS→One UI mapping is android-ui-spec.md.

Versioning (RULE — clean-room app com.astradial.phone)

Format shown in Settings → footer: Major.minor.patch [build] (e.g. 0.1.0 [2]), sourced from app/build.gradle.kts:

  • versionName = Major.minor.patch (semver): patch = bug fixes; minor = new backward-compatible features; major = a breaking/marquee release. Play Store shows this.
  • versionCode = the [build] — a monotonically increasing integer, the [N] in the footer. Play requires every uploaded APK/AAB to have a strictly higher versionCode than any prior upload.

Bump rule (every build you ship/hand to a tester): 1. Always increment versionCode by 1 (so "is this the latest build?" is answerable from the Settings footer — never two different builds sharing a [N]). 2. Bump the matching versionName segment when it's a feature/fix release (patch for fixes, minor for features); leave it for a pure rebuild. 3. Note the bump in the commit message.

Current: 0.4.4 [10] (Internal-testing build with the security hardening). The Settings footer reads BuildConfig.VERSION_NAME/VERSION_CODE, so the number is always the truth of what's installed. (Debug builds share a [N] across quick rebuilds — bump versionCode whenever you install a build a tester will actually use.)

Goal (one line)

Make the Android app match the Astradial iOS app screen-for-screen and flow-for-flow, built entirely in native Samsung One UI (Kotlin + XML + Material Components themed to One UI). Keep Linphone's engine + view-models; replace views/layouts/navigation.


✅ 2026-07-15 — One UI redesign, iOS analytics parity, call bug fixes, security hardening, Play upload

Clean-room app com.astradial.phone (Compose, own AstraSIP+WebRTC engine). All on device (V2037), tests green.

Design / UX (full Samsung One UI): - App-wide theme: brand container tokens (kills stray M3 lavender), type ramp, shared OneUiCard (white + 1dp shadow) / OneUiHeader / InitialAvatar (8-tone palette) / OneUiDivider, motion pass (tab cross-fade, detail slide-in, dialpad FAB, Pulse expand). - Nav 5→4 tabs: Recents + Keypad merged into Phone (recents list base + dialpad bottom sheet that collapses on scroll-up). Contacts / Tickets / Pulse are the others; every tab self-owns its header. - Recents row redesign (direction-icon avatar, clean CLID parse — no "…" <…>, Label · time · ext), scoped to the signed-in user's own extension (client guard over ?extension=mine), To/From party. - Detail pages: call-info (from Recents), iOS-style contact detail, ticket detail (#45 layout). - Contacts Google-style per-contact cards + status toggle; Settings professional grouped rows (icon chips + minimal profile card); single OS splash (dropped the redundant in-app splash). - Pulse = iOS AstradialMD fitness-tile parity: answer-rate activity ring hero + 2×2 Missed/Recovered/ At-risk/New grid + Trend tile; PulseMath parity fields (todayAnswered/Missed, yesterday, newCallersLast7).

Call bugs fixed (real-device, own engine) — see Errors 97–100 in troubleshooting.md: - One-way audio → RECORD_AUDIO requested at call time (was a one-time/permanent-deny startup request). - Incoming ringtone → self-managed Telecom rings itself (looping MediaPlayer + vibrate) + Answer/Decline notification actions; decline sends 603 Decline (not 486) + wake-race bind guard; proximity screen-off.

Release / Play: - Release signing wired (signingConfigs from gitignored keystore.properties; Play App Signing enabled — Google holds the signing key, 1million keystore is just the resettable upload key). R8 OFF for v1 (behaves like the tested debug build; enable with keep-rules + retest pre-GA). - Security audit (must-fix all closed): IncomingCallActivity + WakeReceiverexported=false (were remotely answer/reject-able + spoofed-caller injectable), allowBackup=false, normalizeApiUrl allowlists *.astradial.com (custom-server token-harvest), SIP-trace/caller logs DEBUG-gated. Verified clean: no cleartext/trust-all, DTLS-SRTP, tokens in EncryptedSharedPreferences, no committed secrets. - AAB 0.4.4 [10] built + uploaded to Internal testing. Firebase release + Play-app-signing SHAs added. Listing copy/permissions/data-safety draft: android-play-listing.md.

Next: verify a call from the Internal-testing install → promote to Production; finish App-content forms (Data Safety, content rating, demo login for App access) + graphics (512 icon, feature graphic; screenshots ready, padded to 2:1). Pre-GA hardening: R8 keep-rules, TLS pinning, stronger keystore passphrase.


✅ DONE & verified (earlier sessions — engine cutover / linphone-fork era)

Repos & wiring

  • astradial-android-oss (PUBLIC GitHub fork of BelledonneCommunications/linphone-android, branch astradial, secret-scanning + push-protection + Dependabot ON).
  • astradial-android (PRIVATE, standalone, branch private-main to be created).
  • Local clone: ~/AstradialDevelopment/Mobile App/astradial-android, branch astradial, remotes upstream=Belledonne · oss=public · origin=private (gh auto-added upstream). Taken directly from current upstream (the old astradial-softphone-android is disregarded).
  • ⚠️ Local code changes are NOT yet committed/pushed (see "Commit/push" below).

Build toolchain (hard-won — see gotchas)

  • JDK 21 = Android Studio JBR /Applications/Android Studio.app/Contents/jbr/Contents/Home.
  • SDK ~/Library/Android/sdk; installed cmdline-tools;latest, platforms;android-37.0, build-tools;37.0.0 + 36.0.0; symlink platforms/android-37 → android-37.0 so AGP's compileSdk=37 resolves. local.propertiessdk.dir (gitignored).
  • Build: JAVA_HOME=<JBR> ANDROID_HOME=~/Library/Android/sdk ./gradlew :app:assembleDebug (keep the daemon — do NOT pass --no-daemon). Cold build ~19 min; incremental ~5–15 s.
  • Linphone SDK 5.5.5-pre.3 AAR from download.linphone.org/maven_repository (no NDK/local SDK).

Emulator

  • AVD Pixel_9_Pro_XL_API_VanillaIceCream = emulator-5554 (Android 15). Boot: ~/Library/Android/sdk/emulator/emulator -avd Pixel_9_Pro_XL_API_VanillaIceCream.
  • Install/run: adb install -r <apk>; adb shell monkey -p org.linphone -c android.intent.category.LAUNCHER 1.
  • Screenshot: adb shell screencap -p /sdcard/x.png && adb pull /sdcard/x.png /tmp/x.png (works in debug).

P0 branding (screenshotted ✔)

  • App name Linphone → Astradial: res/values/strings.xml L4 <!ENTITY appName "Astradial">.
  • Launcher icon: regenerated mipmap-{m,h,xh,xxh,xxxh}dpi/{ic_launcher,ic_launcher_round,linphone_launcher_icon_foreground}.png from ~/AstradialDevelopment/AppIcons (8)/appstore.png (1024²) via PIL; adaptive mipmap-anydpi/ic_launcher{,_round}.xml bg → @color/ic_launcher_background (#1900FF), monochrome removed.
  • Brand recolor (global): res/values/colors.xml orange_main_500 #FF5E00 → #1900FF, orange_main_100 #FFEACB → #DBD7FF, added ic_launcher_background #1900FF.
  • FLAG_SECURE debug guard: ui/GenericActivity.kt enableWindowSecureMode()enable && !BuildConfig.DEBUG (so emulator screenshots work in debug; release keeps secure mode).
  • Splash theme: res/values/themes.xml AppSplashScreenTheme bg → blue, icon → Astradial.

Files edited: GenericActivity.kt, strings.xml, colors.xml, themes.xml, mipmap-anydpi/ic_launcher{,_round}.xml, the mipmap-* PNGs, CorePreferences.kt.

Dialer-only nav (done)

CorePreferences.disableChat/disableMeetings default → true → Chat + Meetings tabs hidden. Verified: onboarding shows "To fully enjoy Astradial…", blue OK button + mountains (rebrand confirmed on real UI).

P1 — Onboarding Landing (done, verified)

assistant_landing_fragment.xml rebuilt into the iOS choice screen: Astradial logo + "Astradial Phone" + subtitle, Sign In (→ SIP connect via thirdPartySipAccountLoginClickListener) + Skip login (→ backClickListener, leaves assistant to the dialer). LandingFragment.kt untouched (kept the <variable>s + a hidden @id/password). Verified on emulator: Landing renders, Sign In → terms dialog → SIP login, Skip → dialer. Reached via drawer → "Add an account" (initial onboarding is Permissions → main; Landing is the add-account flow).

P1 — "Connect Your Line" SIP screen (done)

assistant_third_party_sip_account_login_fragment.xml retitled "Connect Your Line", button "Connect", removed the Linphone "mountains" decoration. Kept binding deps (binding.transport Spinner, binding.password). Build-verified. Next P1 polish: default the domain/transport to Astradial (UDP devsip.astradial.com), One UI field styling.

Splash — fully rebranded (done, verified)

⚠️ Gotcha: the splash theme AppSplashScreenTheme is overridden per API in values-v31/, values-v33/, and values-night*/themes.xml — editing base values/themes.xml alone has NO effect on API 31+. Fixed ALL variants: brand-blue background + Astradial icon (@mipmap/linphone_launcher_icon_foreground), dropped the fox @drawable/linphone_splashscreen + windowSplashScreenBrandingImage ("linphone" wordmark). Verified on API 34: blue splash + cream Astradial "a", matches iOS.

P2 — One UI white header + status bar (done, verified)

Flattened Linphone's brand-filled header → One UI white: main_activity_top_bar.xml (bg ?attr/color_main2_000, title ?attr/color_text, icon style tint ?attr/color_main2_600), and the Calls/Contacts fragment root backgrounds blue→surface. Status bar white + dark icons. ⚠️ Edge-to-edge gotcha (Android 15 / targetSdk 37): android:statusBarColor is ignored — the strip behind the transparent bar is main_activity.xml's notifications_area background (was @drawable/color_main1_500; → color_main2_000), and the icon colour is forced in MainActivity.onCreate SystemBarStyle.auto{…} (was hard true/white → now follows light/dark mode). Verified API 34: white status bar + white "Calls" header, dark text/icons. Keypad assessed: linphone's "keypad" = the New-call screen (StartCallFragment, contact search) with a numpad bottom sheet (start_call_numpad_bottom_sheet); the real keys are call_numpad_digit*.xml + shape_squircle_numpad_background (solid @color/gray_500, white digits). After the rebrand it already looks clean (white One UI header carries through, dark squircle keys, green call FAB). ⚠️ The same numpad is reused by the in-call DTMF pad on a DARK background, so its dark-key/white-digit scheme is a deliberate cross-context choice — a "lighter One UI" keypad needs separate light/dark numpad theming, not a one-line recolor.

✅ FROM-SCRATCH One UI rebuild (done & verified — supersedes the re-skin)

Per Hari: stop re-skinning Linphone, build the Samsung One UI from scratch wired to the engine (spec: android-ui-reference.md). Done + screenshotted on the emulator: - Design system: res/values/oneui_colors.xml + drawables (oneui_card_bg, oneui_nav_pill_bg, oneui_nav_selected_pill, oneui_avatar_circle), numpad_oneui_key. - Floating pill nav oneui_bottom_nav.xml (Keypad·Recents·Contacts, grey selected pill). - Keypad KeypadFragment+keypad_fragment.xml — white, thin numbers + grey letters, green round call button, dialed number = viewModel.searchFilter; start destination; keys spread across the lower screen (40% guideline + weighted rows). - Recents RecentsFragment+recents_fragment.xml+recents_cell (HistoryListViewModel.callLogs via the entries binding adapter, DataBindingUtils.kt:91). Now fleshed out + verified (see "Contacts & Recents fleshed out" below). - Contacts ContactsFragment+contacts_fragment.xml+contact_cell (ContactsListViewModel.contactsList). Now fleshed out + verified (see below). - Nav: new main_nav_graph destinations keypad/recents/contacts; tab nav = popUpTo keypad+singleTop. ⚠️ Gotchas: a Drawable/int ternary in a background="@{...}" crashes (ClassCastException) — use @drawable/transparent_background, not @android:color/transparent. setEntries binds BR.model per cell. Commits 32efb6a0(keypad+nav+design) · 3795120d(keypad spread) · 7a1882a7(recents+contacts).

✅ Contacts & Recents fleshed out (done & verified with data)

Earlier these were title-only stubs. Now both match the Samsung reference and render with data: - Contacts (contacts_fragment.xml + contact_cell.xml): big centred "Phone" title + "N Contacts" count subtitle (@{viewModel.contactsList.size()}); three header cards No profile info / Add your favorite contacts / Groups (colored circle icons: oneui_person/oneui_star/users_three on oneui_circle_fill tinted orange/amber/grey); A–Z section letters; per-contact gradient avatars; tap a contact → inline expand to "Mobile " + a Call (green) + Info (grey) action row. No message/video buttons (per Hari). Call → ContactAvatarModel.startCall() (interpretUrl(number,true)startAudioCall) — verified placing a real outgoing call. Info → opens the contact in the system Contacts app via friend.nativeUri (the Linphone contact-detail screen lives in a separate nested nav graph the new dialer doesn't host — revisit when a One UI detail screen exists). - Recents (recents_cell.xml): per-contact gradient avatar (unified with Contacts), name + "Today | HH:MM" + color-coded direction icon (green=incoming answered, blue=outgoing answered, red=missed/aborted — the getCallIconResId drawables already carry these colors, so we removed the grey app:tint instead of forcing one); row tap calls back; empty-state ("No recent calls", phone_list icon) when viewModel.callLogs empty. - New plumbing: DataBindingUtils.setSectionedContactEntries (sectionedEntries/layout/ listViewModel → sets BR.model+BR.header+BR.viewModel, emits the A–Z letter on the first contact of each group) and setAvatarSeed (stable gradient by name hash, drawables oneui_avatar_grad_1..6). ContactAvatarModel gained expanded/toggleExpanded()/ displayNumber/startCall(); ContactsListViewModel gained contactInfoClickedEvent/ onContactInfoClicked(). - Native contacts load fix (MainActivity.onResume): Linphone only imported native contacts on first account registration (triggerNativeAddressBookImportRegistrationState.Ok). A dialer should show contacts even with the line down, so we now call loadContacts() once on resume when READ_CONTACTS is granted, regardless of registration. - 🔧 Test data on the emulator: READ_CONTACTS/WRITE_CONTACTS granted via pm grant; 12 native contacts inserted with content insert (⚠️ multi-word names must be device-shell single-quoted — --bind "data1:s:'Emma G'" — else the on-device shell re-splits them; and this Android's content has no --sort, so take the max _id host-side). Call logs live in files/linphone.db (not call-history.db); delete it to test the Recents empty-state. To seed varied Recents test data, insert into conference_call + sip_address (core.callLogs reads them directly — no event linkage needed): direction 0=out/1=in, status 0=success/1=aborted/2=missed, start_time in UTC (device is IST, +5:30), remote sip:<phoneNumber>@devsip… so contact resolution matches. No adb root on this image, so push the edited db back via chunked base64 through run-as (writing into the app dir keeps the SELinux context); /tmp/load_contacts.sh, /tmp/calllogs.sql, /tmp/push_db.sh capture the flow. - Build green; NOT yet committed/pushed.

✅ Branch model now live (OSS astradial + private private-main)

Per repo-cicd: astradial = OSS Firebase-free dialer; private-main = OSS + overlay (Firebase/Tickets/Analytics). One-way OSS→private. Committed on astradial (then fast-forwarded into private-main): ada6461e9 (One UI dialer: Settings/Contacts/ Recents/dark-mode/arrows) and 1f1e5582b (onboarding). Not pushed — push to oss/origin stays Hari's action.

✅ Phase 1a — authless onboarding (OSS, done & verified)

Removed the Linphone.org login from the assistant: RegisterFragment, RegisterCodeConfirmationFragment (phone/SMS), RecoverAccountFragment, ThirdPartySipAccountWarningFragment + layouts (incl. sw600dp) + their nav destinations. LandingFragment "Sign In" → straight to the One UI "Connect Your Line" SIP screen (QR + Skip kept). Astradial onboarding defaults in linphonerc_factory ([ui] assistant_third_party_sip_account_transport=udp, ..._domain=devsip.astradial.com). Verified on emulator: Welcome→Skip→Permissions→Landing→Sign In→Connect-Your-Line with Domain prefilled devsip.astradial.com, Transport UDP. Dark-mode auto-reset finished in GenericActivity (always re-applies the saved pref → "auto" follows system).

✅ Phase 1b — org email/password sign-in + SIP auto-provision (private overlay, built)

On private-main, package org.linphone.astradial.* (private-only). Commit bf83fea93. - App login = organisation email + password (Firebase), SIP provisioned automatically after. Landing "Sign In" → AstradialSignInFragment (email/pw) → FirebaseAuthPOST /api/v1/auth/user-login (Firebase ID token → 24h org JWT) → AstradialSipLineFragment lists org SIP lines (GET /users) → select → GET /users/:id/sip-credentialsAstradialSipProvisioner registers the Linphone account (UDP, devsip). "Use SIP credentials instead" = manual fallback. The OSS Landing still routes "Sign In" → manual SIP (LandingFragment + assistant_nav_graph diverge private-only — a deliberate merge surface). - Security: JWT + user in EncryptedSharedPreferences (SecureStore, AES-256-GCM/Keystore), never logged; OkHttp HTTPS-only API client (AstradialApiClient); session/token lifecycle + 401 re-exchange in AstradialSession. Deps added (private overlay): firebase-auth, okhttp. - Verified to the limit without the real config: the email/pw screen renders, Landing routes to it, and a sign-in against the placeholder Firebase project fails gracefully (ERROR_OPERATION_NOT_ALLOWED → inline "Incorrect email or password", no crash). - ⏳ Blocked for the success path: needs the misssellerai Android google-services.json (add an Android app org.linphone + org.linphone.debug to that Firebase project, enable Email/Password sign-in). Drop it at app/google-services.json then gitignore + skip-worktree. Until then the real auth→JWT→line-picker→provision chain can't be run.

✅ Phase 2 — Tickets page (private overlay, built & verified)

On private-main, org.linphone.astradial.tickets. Commit 2eb24ab27. Mirrors iOS TicketsUI. - List (TicketsFragment/TicketsViewModel): Open/In progress/Closed/All segmented filter with status_counts; rows = gradient avatar, name/number, URGENT/HIGH badge, "N missed calls" summary, callback line, relative time. Sample-data fallback behind a connect banner when not signed in (offline-demoable, like iOS). - Detail (TicketDetailFragment): header, Call Back (interpretUrl→startAudioCall), Status & Priority chips (PATCH /tickets/:id, remark on status change), call timeline (GET /tickets/:id/events). - New ticket (NewTicketFragment): number/name/summary → POST /tickets (manual). - API + AstradialSession gained ticket methods; generic entriesWithVm binding adapter forwards the VM to cells. Reached via a "Support tickets" row in One UI Settings (private-main divergence) — the gated bottom-nav tab placement lands with Analytics in Phase 3. - Verified on emulator (sample path): segments/counts/badges, detail chips + call-back, new-ticket form. Live data needs sign-in (misssellerai config).

✅ Login verified end-to-end + post-login refinements

  • Live login works. Real org sign-in confirmed on emulator: Signed in [thangaveluhospital@gmail.com] role [owner] org [Thangavelu Hospital], session restored on restart. The app installs as com.astradial.phone (Firebase project misssellerai, real google-services.json provided + skip-worktree'd). Bad creds → ERROR_INVALID_CREDENTIAL (project/email-pw correct).
  • Tickets is a gated bottom-nav tab now (not a Settings row): oneui_bottom_nav got a Tickets item shown when AstradialSession.isSignedIn (observed reactively in Keypad/Recents/Contacts fragments); TicketsFragment became a top-level tab (bottom nav, no back). Loads real org tickets (segments/counts live, no sample banner once signed in).
  • Settings cleanup: removed the "Connect your line" (redundant post-login) and "Support tickets" rows from oneui_settings_fragment.
  • ⋮ → gear settings icon on the three dialer tabs.
  • No camera prompt: dropped CAMERA from Compatibility/Api33Compatibility.getAllRequired PermissionsArray and hid the camera row (cameraPermissionGranted defaults true). Manifest declaration kept (harmless; QR/video would request at point-of-use only).
  • Ticket data fix: JSONObject.optString returns the string "null" for JSON null — added strOrNull; title falls back to number→"Unknown caller"; JSON notes like {"remarks":"…"} now display the remark.

✅ Phase 3 — Pulse Analytics (private overlay, built & verified with real data)

org.linphone.astradial.analytics. Health-style dashboard per md-analytics, verified live for Thangavelu Hospital (owner). - Gated bottom-nav tab "Pulse" (5-tab: Pulse·Keypad·Recents·Contacts·Tickets, 72dp each). Shown only when AstradialSession.canSeeAnalytics (role in owner/admin/supervisor — set in the Keypad/Recents/Contacts/Tickets fragments alongside the Tickets gate). - Cards: Answer-rate ring (PulseRingView, Canvas; target 95% → green/orange/red, derived as (total−missed)/total since the API's answered can equal total) + calls/missed/₹-at-risk; Recovery discipline (recovered % + median pickup + insight); Call volume bar chart (PulseBarChartView, Canvas) + week-over-week / per-day insight. Range selector 7d/30d/90d. - Data: GET /calls/stats?range= (server-aggregated) + /tickets counts via AstradialSession. Gotchas fixed: per-day entries are {date,inbound,outbound} (no total → derive inbound+outbound); insight TextViews were 0dp-wide (→ match_parent). Sample fallback behind a banner when offline. - ⚠️ Revenue-at-risk uses ₹150/patient (md-analytics default; make a Settings stepper later). Remaining md-analytics cards (hourly-by-window, new-patients, all-calls+recording list) are follow-ups; the decision-first core (ring + recovery + volume) ships.

✅ FCM wake-push pipeline (private overlay, built 2026-07-07 — the P5 tail)

Commit dc6278358 on private-main. Contract = platform PR #500 (live on staging+prod), identical to iOS registerVoipToken/ackVoipPush in AstraCall/Screens/AstradialMD.swift. - Token side (org.linphone.astradial.push.AstradialPush): FCM token → POST /users/voip-push-token {asterisk_endpoint, token, environment:'dev'|'prod' (by build type), bundle_id: BuildConfig.APPLICATION_ID, platform:'android'}, keyed by the default account's SIP username; no-ops signed-out/pre-provision. Triggers: KeypadFragment signed-in observer (restore + sign-in), SIP-provision success (AstradialSipLineFragment), and token rotation (onNewToken). API/auth plumbing in AstradialApi.kt + AstradialSession.registerVoipPushToken. - Wake side (AstradialFcmService, replaces the inert linphone-sdk FirebaseMessaging service in the manifest): on push {caller, push-id} it acks FIRST (POST /users/voip-push-ack {push_id}, unauth'd — 5s gateway deadline) then core.refreshRegisters() so the INVITE arrives via liblinphone's normal incoming path. ⚠️ Never inject a placeholder call into the Core — the pre-dial push has no call-id and liblinphone dedup kills the real INVITE (the iOS bug, see voip-push-wake.md). Stale pushes are killed server-side (android.ttl=0s). The full ConnectionService-placeholder wake (§11 Wake-v2) belongs to the clean-room astradial-astracall-android engine, not this fork. - Verified (emulator, fresh install): AstradialFcmService owns MESSAGING_EVENT (dumpsys), FCM minted a token → onNewToken fired → signed-out guard skipped the POST; prod route contract probed read-only (voip-push-token → 401 auth gate, voip-push-ack → 200 unauth'd, both as designed). app/google-services.json (misssellerai, com.astradial.phone) already present + skip-worktree'd — not a blocker. - ⏳ Blocked-by: (1) signed-in token POST needs a human sign-in on a real device/emulator (no credentials available to an agent; stagepbx is behind CF Access so the app can only reach prod); (2) full wake E2E needs the server FCM sender finished — api/src/services/fcmService.js is a stub (firebase-admin is already a dep + initialized with the misssellerai SA in server.js, so the send is ~10 lines; being handled platform-side).

✅ Account management + FCM sender live (private overlay, 2026-07-07 late)

  • Server FCM sender shippedapi/src/services/fcmService.js implemented (real admin.messaging().send({token, android:{priority:'high', ttl:0}, data}) via the already-init'd firebase-admin, GOOGLE_APPLICATION_CREDENTIALS/misssellerai — no new creds). Platform PR #511 merged to staging + deployed + verified configured on the staging box (firebase-sa-key.json present, sender code live). ttl:0 = now-or-never (the phantom-call guard, iOS apns-expiration:0). Not yet promoted to prod (staging only).
  • Sign Out (f747d66cb) — Settings → Sign Out clears the AstradialSession (Firebase) + the provisioned liblinphone Core account, returns to Landing. Fixes the "stuck in one org, no way out" gap iOS's account switcher already had.
  • Change SIP line (8ecde3791) — Settings SIP-line row got a Change button → org picker / manual creds / QR scan (the iOS "Connect Type" trio). ⚠️ KNOWN BUG (unfixed): changing the SIP user currently forces a re-login — it shouldn't; a same-org extension change must reuse the existing signed-in session (only Sign Out should return to login). Fix was queued to the overlay agent but it hit its API session limit (resets 2026-07-08 02:50 IST) before doing it.
  • Notification permission — banner "Post notifications permission not granted!" made actionable: tap → POST_NOTIFICATIONS system dialog (Android 13+), verified showing on emulator. Uncommitted (MainActivity.kt in the working tree) — agent died mid-verify; needs commit on resume.
  • Login flow redesign — Hari wants the login page reworked professional/neat with no intro page; requested this session, pending (overlay agent out of session).

Engine cross-ref: the clean-room own-engine (astracall-android-plan.md) passed Phases A+B+C on 2026-07-07 — register, 2-way audio, killed-app wake, all on AstraSIP+WebRTC (no liblinphone). That engine, not this fork, is where call quality / hangup / encryption-tag / call-UI get fixed — at cutover (Phase F). Two emulators now in use: emulator-5554 = Hari's manual testing, engine_avd/emulator-5556 = agents' builds (split to stop build-churn ANRs).

✅ 2026-07-08/09 — login redesign, engine cutover (F1+F2), audio RCA

Login/onboarding redesign (mockup #24 → then #37 for call UI): intro/welcome carousel removed (cold launch → login directly); sign-in rebuilt clean (wordmark, "Get Started!", grey pill fields, black/white ink button, light+dark); Sign out on the no-org dead-end; pre-login server dropdown (Production—devsip / Staging—stagesip / Custom, persists api_base_url) — fixes the chicken-and-egg where "change API server in Settings" was unreachable pre-login; permission red bar → rounded amber toast + auto-request notifications on open; Contacts search + SIM Calls Only toggle (click-to-call + routing, iOS parity) with a glass SIM-number dialog. Meetings UI removed (31 files; chat+conference LEFT — too entangled with the call/notification path, cutover-subsumed). DTMF keypad-tone buzz fixed (linphone opened a fresh AAudio stream per keytap → native ToneGenerator).

Engine cutover — Phase F1+F2 (branch private-main, rollback tag pre-astrasip-cutover): - F1 — lifted astrasip + WebRTC + Telecom glue into the app as AstraEngine (namespace com.softphone.astradial.engine); rewired the provisioner/push/telecom seams; built DTMF + hold. Build green; registers e1003ra9v72 on staging wss (REGISTER→200), call INVITE→200→ACK→BYE. liblinphone NOT deleted (waits for the real-device audio gate). - F2 — new AstraCallActivity (4 states, mockup #37: white, circular avatar/controls, red hangup / green accept / blue speaker), bound to AstraEngine.onCallState; in-call DTMF numpad; hangup/stuck fix. - AUDIO RCA (evidence-based): the engine's 2-way WebRTC audio is packet-proven (native x86_64 run → staging *43: out 600 / in 586 pkts, ICE+DTLS clean, matches iOS). Two real bugs fixed: (1) FGS crashMicForegroundService.start() while app not foreground → ForegroundServiceStart NotAllowedException killed the call (b4f4a4a84); (2) emulator "no audio" = arm-on-x86 artifact — app ships arm-only, ran under translation on x86 emulator → libsrtp init fails → drops all RTP; fixed by adding x86_64 to debug builds. ⚠️ A prior agent FALSELY claimed "audio works on a real device" — it was the emulator mic-off + this artifact; distrust unverified agent audio claims. - STILL real-device-pending: moto g34 *43 audio (native arm64 — translation N/A), and the prod devsip media path (only ever proven with iOS on staging). Then delete liblinphone → Phase 3 clean-room.

GPL compliance: full audit + roadmap → gpl-compliance-roadmap.md. Key finding: removing linphone-sdk-android:5.5.+ (the cutover) is necessary but NOT sufficient — the app is a GPLv3 fork (243 fork .kt files); true GPL-free = the clean-room app (Phase 3), porting our 20 org.linphone.astradial.* files + the AstraEngine glue as our code.

Prod infra: enabled the WSS listener on prod Asterisk (devsip:8089/ws) — the hidden blocker for the iOS App Store launch too (see prod WSS enablement); flipped Om Chamber ext 1001 → sip_port='wss' for the real call test.

✅ Dark mode + professional call-direction arrows

  • Dark mode (res/values-night/oneui_colors.xml): the One UI tokens existed only in values/ (light), so every from-scratch screen stayed light when the app ran in dark mode. Added a night override for oneui_canvas(#0B0B0C) / oneui_card(#1C1C1E) / oneui_text_primary / oneui_text_secondary / oneui_divider / oneui_pill_selected; the colored avatar/icon/accent tokens fall back to values/ (fine on dark). Card/nav-pill drawables already reference @color/oneui_card, and the keypad digits use Linphone's ?attr/color_text/?attr/color_main2_500 (which have night variants), so they adapt too. Verified all four screens dark on the emulator. The app's light/dark/auto choice lives in Settings → User interface (config [app] dark_mode: -1 auto / 0 light / 1 dark; applied by GenericActivity.onCreate). ⚠️ Test gotcha: this emulator had dark_mode=0 (forced light), so cmd uimode night yes did nothing until dark_mode was set to 1/-1.
  • Call-direction arrows: Linphone's incoming_call*/outgoing_call* glyphs were awkward 12dp filled "corner" shapes. Replaced all six with the standard Material call_received(↙) / call_made(↗) diagonal arrows (24dp viewport), keeping the colour code green=incoming-answered / blue=outgoing-answered / red=missed-or-aborted.

✅ Settings (done) + UX fixes

  • Settings OneUiSettingsFragment + oneui_settings_fragment.xml (One UI cards: SIP line status from core.defaultAccount, "Connect your line" → AssistantActivity, About). Opened from every tab's (onMenuClicknavigate(R.id.oneuiSettingsFragment)). ⚠️ The old Linphone settings_fragment.xml was accidentally overwritten then git checkout-restored — the One UI one is oneui_settings_fragment.xml (distinct name) to avoid clobbering it.
  • All Linphone settings features now reachable from the One UI Settings page (req: "bring all the feature in the settings page of linphone"). Added a grouped One UI card with rows that safeNavigate(destId) into the existing engine settings screens (no feature rebuilt, none lost): Calls, network & displayR.id.settingsFragment (Security · Calls · Contacts/LDAP/CardDAV · Network · User interface · Advanced-settings button); AdvancedR.id.settingsAdvancedFragment; DeveloperR.id.settingsDeveloperFragment. Linphone bundles Calls/Network/Security/Contacts/ Conversations/Meetings/UI into the single settingsFragment (collapsible sections, no per-section deep-link) so one row covers them all. Verified on emulator: each row opens its screen, back returns to the One UI Settings page. New binding vars onAllSettingsClick/ onAdvancedClick/onDeveloperClick + safeNavigate() (swallows the double-tap nav race). Build green. NOT yet committed/pushed.
  • 🔧 Reaching the dialer to test: a fresh emulator has no SIP account → MainActivity redirects to AssistantActivity (MainActivity.kt:407). To render the dialer/Settings without real creds, inject a throwaway account into /data/data/org.linphone/files/.linphonerc ([proxy_0] devsip UDP + [auth_info_0] + default_proxy=0, set reg_sendregister=0 to suppress the auth dialog) via base64 + run-as (stdin//data/local/tmp are blocked by SELinux). Shows a harmless red "account disabled" banner.
  • UX fixes (commits 48d46bf, de212f2): keypad key ripple now small/contained (oneui_key_ripple); DTMF tone fixed-duration (250ms) so fast taps still beep (NumpadModel); the "square box" on nav press was transparent_background's square state_pressed → replaced with rounded oneui_nav_item_ripple (diagnosed by frame-by-frame screen recording). Commits: c943019(settings) on origin/astradial.

REMAINING — delete the old Linphone UI (risky, needs full headroom): the old main fragments (historyListFragment/contactsListFragment/startCallFragment/conversations/ meetings/DrawerMenuFragment) are no longer shown (pill nav uses the new fragments) but still exist + are cross-referenced by MainActivity, nav-graph actions, and SafeArgs *Directions. Removing them cleanly = remove their <fragment>+actions from main_nav_graph, delete the fragment classes + layouts + adapters + bottom_nav_bar.xml/main_activity_top_bar.xml, and fix every MainActivity/AbstractMainViewModel reference — keep all *ViewModel/*Model/core. Then: in-call One UI; private overlay (Analytics/Tickets/Firebase).

✅ Committed + pushed (private origin/astradial)

2eb3d94 P0 rebrand · 8c786c5 dialer-only nav · 6a99794 P1 Landing · f5b4d93 Connect-Your-Line + splash · ce63b90 One UI header · 526e975 One UI status bar. Hari still needs git push oss astradial. internal-docs main: all docs live on the wiki.

Onboarding flow now (verified):

Permissions → main dialer; drawer → "Add an account" → Landing [Sign In | Skip] → Sign In → terms dialog → Connect Your Line (SIP). Cold-launch splash = Astradial blue.


⏳ PENDING — the One UI rebuild (P1–P5)

Each screen = layout rewrite → build → screenshot-verify. Mapping/notes in android-ui-spec.md. Keep VMs; replace views.

  • P1 — Onboarding (iOS flow): rebuild ui/assistant Landing → [Sign In | Skip]SIP connect (QR/manual) in One UI. Clears the residual "linphone" wordmark on the Android-12 splash (drawable/linphone_splashscreen.xml + linphone_splashscreen_branding.png) and the welcome carousel (welcome_activity.xml + welcome_page_*).
  • P2 — Tab host + Keypad: One UI bottom nav; reorder to Recents/Contacts/Keypad, hide chat+meetings. Keypad = One UI squircle (ui/main/history/StartCallFragment + start_call_fragment.xml).
  • P3 — Recents + Contacts: One UI ui/main/history/HistoryListFragment + ui/main/contacts/ContactsListFragment (org directory; drop READ_CONTACTS).
  • P4 — In-call: One UI ui/call/* (Active/Incoming/Outgoing/Ended); Telecom system screen; DTMF sheet.
  • P5 — Private overlay: Firebase sign-in + Analytics/Pulse + Tickets tabs + FCM push. Needs the Astradial google-services.json (the clone currently has Belledonne's).
  • Cross-cutting: flatten Linphone's orange filled top bars → One UI white/large collapsing headers (the most un-native element); remove Linphone's 10 brand themes.
  • Clean-split: on astradial branch delete app/google-services.json (Firebase auto-off, OSS dialer); private re-adds the real one. (Build auto-toggles Firebase on file presence.)
  • Commit/push: commit local changes to origin (private) astradial + create private-main; Hari runs git push oss astradial (Claude does not push public). See repo-cicd.

Key findings for the rebuild

  • Hide chat/meetings: CorePreferences.disableChat (ui/disable_chat_feature) + disableMeetings (ui/disable_meetings_feature) at CorePreferences.kt L388-393 → consumed in AbstractMainViewModel.kt L334-340 (hideConversations/hideMeetings). Force these true (factory linphonerc or default) to drop the Chat/Meetings tabs.
  • Bottom nav is a custom layout res/layout/bottom_nav_bar.xml (AppCompatTextViews, not a BottomNavigationView): order Contacts / Calls(navigateToHistory) / Conversations / Meetings, each viewModel.navigateToX(). Reorder + relabel here.
  • Top bar orange comes from per-fragment headers using ?attr/color_main1_500.
  • compileSdk=targetSdk=37 is a preview (android-37.0); needs the symlink. Java 21 required.

Continuation runbook

cd "/Users/hari/AstradialDevelopment/Mobile App/astradial-android"   # branch: astradial
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
export ANDROID_HOME="$HOME/Library/Android/sdk"
ADB="$ANDROID_HOME/platform-tools/adb"
# boot emulator if needed:
"$ANDROID_HOME/emulator/emulator" -avd Pixel_9_Pro_XL_API_VanillaIceCream &
./gradlew :app:assembleDebug --console=plain                 # ~5-15s incremental
"$ADB" install -r app/build/outputs/apk/debug/linphone-android-debug-*.apk
"$ADB" shell monkey -p org.linphone -c android.intent.category.LAUNCHER 1
"$ADB" shell screencap -p /sdcard/s.png && "$ADB" pull /sdcard/s.png /tmp/s.png   # then view

Related: overview · astradial-android · android-ui-spec · decisions · repo-cicd.