Skip to main content

Android onboarding

You are joining the team that owns the native Android client — the surface that 99% of our users actually see and touch. This page gets you from "git clone" to "first PR merged" in about a week.

If you have not yet read the Onboarding overview, start there. This page assumes you have the 30-minute mental model.


What you are signing up for

The Android app's job, in one paragraph:

Discover call-style recordings that already exist on the device. Let the user explicitly choose what to upload. Run durable uploads via WorkManager that survive app kill and process death. Poll the backend until the call reaches a terminal status. Render a clean transcript with real speaker names and a structured analysis. Never auto-upload anything. Never persist raw audio.

That is the entire job. The interesting design pressure is reliability — uploads must keep going while the user is on the metro with no signal, the app is swiped away, and the phone reboots. Everything else flows from that.


The tech

LayerChoiceNotes
LanguageKotlin 2.0Latest stable. We use coroutines aggressively, sealed types, and value class where it helps.
UIJetpack Compose + Material 3No XML layouts. Custom "glass" theme in ui/theme.
NavCompose NavigationString routes in ui/navigation/ScryonRoutes.kt.
DIHilt 2.51 (with KSP)All singletons live in di/ modules.
NetworkingRetrofit 2.11 + OkHttp 4.12 + Moshi 1.15Interceptors centralised in data/remote/.
AsyncCoroutines 1.9 + kotlinx-coroutines-play-servicesTasks.await(...) for Firebase.
BackgroundWorkManager 2.9 + Hilt-Work + lifecycle-processThe upload critical path.
AuthFirebase BoM 33.7 (Auth) + Credential Manager 1.3Email / Google / Phone providers.
PersistenceSharedPreferences per-user-namespaced + on-disk JSON cacheNo SQLite, no Room (yet).
BuildGradle Kotlin DSL + version catalog (gradle/libs.versions.toml)

Day 1 — get it running

1. Prereqs

  • Android Studio Hedgehog or newer.
  • JDK 17 (Gradle uses JVM 11 target; JDK 17 runs Gradle fine).
  • A Firebase project with Email/Password, Google, and Phone sign-in enabled.
  • Backend access — local Mac backend (docker compose) for day-to-day dev, or STAGING_API_KEY from Railway for testing without running the backend locally.
  • A device or emulator on API 26+.

2. Clone and configure

git clone git@github.com:FluxonLabs/scryon-android.git
cd scryon-android

Drop google-services.json into app/ (gitignored). Create local.properties at the repo root:

sdk.dir=/Users/<you>/Library/Android/sdk

# Local backend (get your Mac's IP: ipconfig getifaddr en0)
DEV_BASE_URL=http://192.168.1.xxx:8080/
DEV_API_KEY=dev-local-key

# Railway staging (for testing without running the backend locally)
STAGING_API_KEY=<ask the team>

# Firebase
FIREBASE_WEB_CLIENT_ID=<id>.apps.googleusercontent.com

Full reference: Android · Configuration.

3. Start the backend and install the app

Both repos ship a dev.sh script for one-command startup.

Terminal 1 — backend:

cd ../scryon-backend
./dev.sh # without pyannote (default, no extra key needed)
./dev.sh --pyannote # with pyannote speaker separation

Before the first run, create scryon-backend/.env.local (gitignored):

export LEMONFOX_API_KEY=your-key
export LLM_API_KEY=your-key

Terminal 2 — Android app:

# from scryon-android root
./dev.sh # builds devDebug → installs as "Scryon Dev"
./dev.sh staging # builds stagingDebug → installs as "Scryon Staging"

Make sure DEV_BASE_URL in local.properties matches your Mac's IP first:

ipconfig getifaddr en0 # e.g. 192.168.1.105
# then set: DEV_BASE_URL=http://192.168.1.105:8080/ in local.properties

Flavor → environment mapping:

VariantBackendUse for
devDebugLocal MacDay-to-day feature work
stagingDebugRailway stagingIntegration testing
prodReleaseRailway prodPlay Store only — never test here

In Android Studio: Build → Select Build Variant → devDebug, then click Run.

If you skip google-services.json, the app still builds — AuthGate renders LoginScreen with a "Firebase not configured" hint. Useful for UI-only changes.

4. End-to-end smoke test

  1. Sign in (email, Google, or phone).
  2. The first GET /api/users/me lazily provisions a backend row.
  3. The Calls tab discovers any call-style recordings on the device. If empty, the empty state has an Allow audio access button.
  4. Tap Transcribe on a recording. Grant READ_CALL_LOG if you like.
  5. Watch the row appear under Transcribed as Uploading → Queued → Transcribing → Analyzing → Completed.
  6. Tap the row. The detail screen loads /api/calls/{id}, /transcript, and /analysis in parallel.

If any of those steps fail, Android troubleshooting is your friend.


Day 2 — walk one upload end-to-end with logcat open

The single most useful thing you can do on day 2 is trace one Transcribe tap through to a completed call with logcat open, filtering by CallUploadWorker, OkHttp, and FirebaseIdToken.

The flow you are following:

Every arrow here has a doc page. The most important ones to internalise:

Key files to open

FileWhy
MainActivity.ktSingle Activity; wraps content in AuthGate.
ScryonApplication.ktHilt entry; supplies the WorkerFactory for Hilt-Work.
data/auth/AuthRepository.ktFirebase wrappers + sign-in / sign-out semantics.
data/auth/FirebaseIdTokenProvider.ktThe token cache. Worth reading slowly.
data/remote/ScryonApi.ktEvery endpoint we consume.
data/remote/interceptor/*ApiKeyInterceptor, FirebaseAuthInterceptor, FirebaseAuthAuthenticator.
data/repository/ScryonRepository.ktThe CallRepository impl — DTO ↔ domain mapping, error mapping, cache.
work/CallUploadWorker.ktThe foreground service that owns the upload critical path.
viewmodel/MainShellViewModel.ktThe polling + status merge logic.
ui/shell/ScryonRoot.ktThe bottom-bar shell.

Day 3 — privacy + conventions

Read these in order:

  1. Privacy & security — non-negotiable. The Android-specific bits are the no-raw-audio rule and the per-uid local-store namespacing.
  2. Android coding conventions in Architecture · Coding conventions.
  3. Permissions — when each permission is asked, and what to do if denied.

Android-specific gotchas to internalise:

  • The UI layer never imports Retrofit, Firebase, or Hilt internals. Repositories return domain models, not DTOs.
  • Every local store is namespaced by Firebase uid. When you add a new store, do this from day one.
  • Raw audio is never persisted locally. Bytes are streamed from MediaStore to OkHttp. Even the voice profile recording goes to cacheDir/voice_profile/ and is deleted immediately after upload.
  • No LiveData. ViewModels expose StateFlow; UI uses collectAsStateWithLifecycle().

Day 4 — pick a first PR

Look for issues labelled good first issue on the repo. Good candidates:

  • A small Compose tweak (empty state copy, an icon, a spacing fix).
  • A new unit test for a ViewModel or a local store.
  • Wiring an existing roadmap item — e.g. notification deep-link (EXTRA_HIGHLIGHT_RECORDING_ID is plumbed; the Calls tab needs the scroll-to logic).
  • A new entry in ScryonError mapping for an HTTP status we don't yet cover.

Stay away from these for your first PR:

  • CallUploadWorker and the foreground-service promotion logic.
  • FirebaseIdTokenProvider and the authenticator chain.
  • Adding a new local store (touches sign-out cleanup in AuthRepository).
  • Anything touching the multipart upload shape.

PR checklist (memorise)

  • Compose previews added/updated for any UI change.
  • No new PII in Log.d / Log.i / Log.w (filter your logs with adb logcat | grep -i scryon before submitting).
  • If you added a SharedPreferences store, it is namespaced by Firebase uid and wiped by AuthRepository.signOut().
  • If you added a new permission, it is requested at the point of use, not at app launch.
  • ./gradlew :app:compileDebugKotlin :app:testDebugUnitTest passes locally.
  • Commit message describes the why, not the what.

Day 5 — ship it

Submit, iterate on review, merge. Congratulate yourself.


Week 2 — own a slice

Pick one of these and become the local expert:

SliceWhere it lives
Auth gate + sign-inui/auth/, data/auth/, Authentication
Upload pipelinework/, data/repository/ScryonRepository.kt, Upload pipeline
Status lifecycle + pollingviewmodel/MainShellViewModel.kt, Status lifecycle
Call detail + cacheviewmodel/CallDetailViewModel.kt, data/local/CallContentCache.kt
Voice profiledata/voiceprofile/, ui/voiceprofile/, Voice profile setup
New-recording notificationsnotifications/, Notifications
Networking layerdata/remote/, Networking

"Own a slice" means: read every file in it, draw your own diagram, run the screens by hand, write a one-pager explaining how it works to someone joining next month.


Week 3 — pair with backend

The Android app and the backend meet at a tight REST contract. Spend a day pairing with someone on the backend team. Have them walk you through how a POST /analyze becomes a row, an artifact, and a COMPLETED status. You will find at least one thing the API could do better. File an issue or open a PR (against either repo).

Reading material for the cross-over:


Week 4 — performance + reliability pass

Take one screen you own and profile it. Use Android Studio's profiler. Measure:

  • Cold start time to the Login screen and to the main shell.
  • Frame jank when scrolling the Transcribed tab with 50+ rows.
  • Memory during a long Compose detail open with a 30-minute transcript.

Pick one win and ship it. We do not have an SLA on these yet — but we should, and you can help define one.


Reference shelf

Bookmark these:


Common stumbling blocks

SymptomLikely causeFix
Build fails with "google-services.json not found"Missing file in app/Drop the file in app/ (gitignored). The plugin is applied conditionally; you can also build without it.
Google sign-in button does nothingMissing FIREBASE_WEB_CLIENT_ID or SHA-1 not registeredAdd the web client ID; register your debug SHA-1 in Firebase Console.
401 — Missing or invalid X-API-Key headerAPI key for the selected flavor is missing or wrongCheck DEV_API_KEY / STAGING_API_KEY / PROD_API_KEY in local.properties, rebuild (BuildConfig is compile-time).
Upload "completes" but row never updatesPointing at one backend but Firebase project verifies tokens from anotherConfirm the backend's Firebase project matches the one in google-services.json.
App appears to "close" right after TranscribeOld build before deferred-foreground fixReinstall current build; worker now waits ~4 s before promoting.
Hilt-related compile errors after a refactorMissing @HiltViewModel / @HiltWorker annotation, or a constructor changeRun ./gradlew :app:kspDebugKotlin --rerun-tasks and re-read the error.

For anything else, Android troubleshooting is more thorough.


What "good" looks like after a month

  • You can describe the upload + status lifecycle without notes.
  • You have shipped at least 5 PRs that touched non-trivial Compose or coroutines code.
  • You have written or improved one piece of documentation in scryon-docs.
  • You have profiled at least one screen and shipped a measurable improvement.
  • You have an opinion about something we should change in the app's architecture — and you wrote an ADR or filed an issue.

Welcome.