HeyBaji is a personal AI voice assistant for Android. The intended experience is:
- You say “Hey Baji”.
- The app answers “Yes, I'm listening.”
- You speak a command (for example, “What is the weather today?”).
- Speech is converted to text, an AI provider produces a reply, and the reply is spoken with text-to-speech.
The UI Activity can be closed while the assistant infrastructure keeps running only when Android allows it: a user-enabled foreground service, granted microphone and notification permissions, and OEM battery policy that does not kill the process. HeyBaji does not fake unsupported platform behavior (unrestricted background microphone, guaranteed boot-time restart, or bypassing battery optimization).
Phases 1 and 2 are implemented:
| Area | Status |
|---|---|
| Multi-module Gradle project, Hilt, Compose navigation | Done |
| Home, Settings, and Conversation screens (scaffold UI) | Done |
Domain models (Message, AssistantError) |
Done |
AssistantState + AssistantStateMachine |
Done |
| Wake word, speech-to-text, TTS, AI provider | Not implemented yet |
| Foreground service, Always Listening, boot recovery | Not implemented yet |
You can install the debug app and navigate the three screens. Voice capture and AI replies are not wired yet.
- macOS, Linux, or Windows with Android Studio Otter / Narwhal or newer (AGP 9.3)
- JDK 17+ (Android Studio’s bundled JDK is enough)
- Android SDK with compile/target API 37
- A device or emulator running Android 7.0 (API 24) or higher
- Network the first time Gradle downloads dependencies
This project uses:
- Gradle 9.5.0 (wrapper included)
- Kotlin 2.2.10
- Android Gradle Plugin 9.3.2
- Start Android Studio.
- File → Open and select this folder (
HeyBaji). - Wait for Gradle sync to finish. If sync asks to trust the project, accept.
- Connect a phone with USB debugging, or start an emulator (Device Manager).
- Select the
apprun configuration. - Click Run (green play button), or press
Shift+F10(Windows/Linux) /Control+R(macOS).
The first launch shows HeyBaji with buttons for Conversation and Settings.
From the project root:
# Debug APK
./gradlew :app:assembleDebug
# Install on a connected device/emulator
./gradlew :app:installDebug
# Unit tests (no device required)
./gradlew test
# Instrumented test (device or emulator required)
./gradlew :app:connectedDebugAndroidTestOn Windows use gradlew.bat instead of ./gradlew.
The debug APK is written to:
app/build/outputs/apk/debug/app-debug.apk
When the remaining phases land, a session will follow this state machine:
Idle
→ wake word “Hey Baji”
WakeWordDetected
→ speak “Yes, I'm listening.”
Speaking (acknowledgement)
→ listening for a command
Listening
→ final speech transcript
Processing (query)
→ AI reply
Speaking (response)
→ Idle
Voice acknowledgement can be skipped (settings: voice response off): WakeWordDetected goes straight to Listening.
Races the state machine already handles:
- Duplicate “Hey Baji” while a session is active is ignored.
- Cancel from any active state returns to Idle.
- Listening timeout (silence) returns to Idle; it is not treated as a failure.
- Processing or speaking timeout becomes a domain
AssistantError. - Late speech/AI events after cancel are ignored.
- UI never receives a raw
Throwable; it getsAssistantError.userMessage.
Timers are not started inside the state machine. The future engine will read timeoutFor(state) and send AssistantEvent.TimedOut.
Clean Architecture + MVVM + unidirectional data flow.
- UI (
feature:*) talks to ViewModels only. - Domain (
:domain:model) holds shared models such asMessageandAssistantError. - Assistant state (
:assistant:state) is the session state machine. Orchestration, voice, data, and the foreground service will be added as real modules when those phases are implemented.
:app
├── :feature:home
├── :feature:settings
├── :feature:conversation
├── :core:designsystem
└── :core:common
:assistant:state → :domain:model
→ :core:common
| Module | Role |
|---|---|
:app |
Application, Hilt root, MainActivity, navigation graph |
:core:common |
Shared utilities (AppDispatchers) |
:core:designsystem |
Material 3 theme and Compose primitives |
:domain:model |
Message, MessageRole, AssistantError |
:feature:home |
Home screen |
:feature:settings |
Settings screen |
:feature:conversation |
Conversation screen |
:assistant:state |
AssistantStateMachine |
| Concern | Library |
|---|---|
| Language | Kotlin 2.2.10 |
| UI | Jetpack Compose, Material 3 |
| DI | Hilt |
| Async | Coroutines, Flow, StateFlow |
| Navigation | Navigation Compose |
| Tests | JUnit, Turbine, AndroidX Test |
Versions live in gradle/libs.versions.toml.
HeyBaji/
├── app/
├── core/
│ ├── common/
│ └── designsystem/
├── domain/
│ └── model/
├── feature/
│ ├── home/
│ ├── settings/
│ └── conversation/
├── assistant/
│ └── state/
├── gradle/libs.versions.toml
└── README.md
Defined in :assistant:state:
Idle— waiting for “Hey Baji” or a manual mic tapWakeWordDetected— phrase heard; about to acknowledge or listenListening— capturing the commandProcessing(query)— AI is runningSpeaking(response)— TTS; acknowledgement returns to Listening, a reply returns to IdleError(error)— recoverable failure with a user-facing message
Domain errors include microphone permission, speech recognition unavailable, network, AI failure, wake-word failure, TTS failure, foreground-service restriction, and battery optimization. Those are mapped to copy the user can hear or read, not stack traces.
| Topic | What Android actually allows |
|---|---|
| Background microphone | Only while a microphone foreground service is running and RECORD_AUDIO is granted. Closing the Activity is not enough by itself. |
| Always Listening | User toggle + persistent notification. The OS can still stop the service. |
START_STICKY |
A hint to restart the service, not a guarantee. |
| Boot recovery | BOOT_COMPLETED may fire before microphone FGS is legal. Recovery must wait for a valid system state. |
| Battery optimization | Must not be bypassed automatically. Settings should explain how the user can exempt the app. |
| OEM skins | Xiaomi, Huawei, Samsung, and others may kill background work anyway. |
| Permission | Why |
|---|---|
RECORD_AUDIO |
Wake word and commands |
POST_NOTIFICATIONS |
Persistent Always Listening notification (API 33+) |
FOREGROUND_SERVICE |
Keep the assistant host alive |
FOREGROUND_SERVICE_MICROPHONE |
Microphone FGS type (API 34+) |
RECEIVE_BOOT_COMPLETED |
Restore Always Listening after reboot if the user enabled it |
The UI must explain each permission before requesting it.
From the project root:
# Everything that does not need a device
./gradlew test
# State machine + domain errors only
./gradlew :assistant:state:test :domain:model:testState machine coverage includes the happy path, duplicate wake word, timeout, cancellation, empty query/response, invalid transitions, and ignored late events.
| Phase | Work |
|---|---|
| 1 | Gradle, Hilt, modules, Compose navigation — done |
| 2 | Domain models, AssistantStateMachine — done |
| 3 | Fake wake word, fake STT, fake AI, Android TTS wrapper (new :voice:* modules) |
| 4 | AssistantEngine (new :assistant:engine module) |
| 5 | Home / settings / conversation production UI |
| 6 | VoiceAssistantService foreground service (new :assistant:service module) |
| 7 | Always Listening preference (DataStore in a new :core:data module) |
| 8 | Boot recovery architecture |
| 9 | Real wake-word provider (for example Porcupine), still behind WakeWordDetector |
Gradle sync fails on Kotlin source sets
The project sets android.disallowKotlinSourceSets=false because KSP 2.2.x still registers generated Hilt sources through kotlin.sourceSets, which AGP 9 built-in Kotlin rejects. That flag is required until Kotlin/KSP move to a 2.3.4+ pair.
SDK / compileSdk 37 missing
In Android Studio: Settings → Languages & Frameworks → Android SDK and install API 37.
Device not listed
Enable USB debugging, accept the RSA prompt, or start an emulator from Device Manager.
Tests fail with a Main dispatcher error
Unit tests use AppDispatchers with Dispatchers.Default instead of Dispatchers.Main. Do not inject Dispatchers.Main on the JVM test classpath.