Kairós is a modern application that transforms your calendar appointments into unmissable full-screen alarms, both on your smartphone and on your wrist with Wear OS. It intelligently synchronizes with the device's calendar and ensures you never miss an important event.
- Full-Screen Alarms: Wakes the device even when locked with visual and sound alerts (Smartphone and Wear OS).
- Full Wear OS Integration:
- Complications: View the next event directly on the watch face.
- Tiles: Instant access to event information with a side swipe.
- Smart Synchronization: Uses WorkManager for efficient background scheduling without draining the battery.
- Native Integration: Reads events from any calendar account configured on the device (e.g., Google Calendar).
- Total Control: Enable or disable alarms globally or for specific events.
This project follows modern Android development principles with MVVM + MVI architecture.
- Language: 100% Kotlin
- UI: Jetpack Compose (Phone & Wear OS)
- Architecture: MVVM + MVI, Clean Architecture, Multi-module
- Async: Kotlin Coroutines + Flow
- Persistence & Background: DataStore, WorkManager, AlarmManager
- DI: Hilt
- AI: Firebase AI (Gemini) with Function Calling
- Sync: Wearable Data Layer (Play Services)
- Testing: JUnit4, Robolectric, Turbine, MockK
kairos-android-app/
├── app/ → Phone UI (Compose), Activity, Receivers
├── core/ → Shared business logic, ViewModels, UseCases, Repositories
├── wear/ → Wear OS UI, Tiles, Complications
├── build-logic/ → Convention plugins (Jacoco, etc.)
└── gradle/ → Version catalog (libs.versions.toml)
The app uses unidirectional data flow across all features:
View (Compose) ──EventIntent──▶ ViewModel ──▶ UseCases / Repositories
▲ │
│ ▼
└──── UiState (StateFlow) ───┘
SideEffect (Channel)
EventIntent— sealed class representing every user action.EventScreenUiState— single immutable state driving the UI.EventSideEffect— one-shot events (snackbar, navigation, confirmation dialogs).EventViewModel— processes intents, delegates to UseCases, emits state & effects.
| Package | Responsibility |
|---|---|
viewmodel |
ViewModels, Intents, UiState, SideEffects, UiText |
usecases |
Business logic (one class per action) |
repository |
Data access (Calendar, Weather, Preferences, etc.) |
model |
Domain entities (Event, AlarmOffset, Weather, etc.) |
service |
AlarmScheduler, Workers |
ai |
AI Agent architecture (see below) |
analytics |
Firebase Analytics abstraction |
The app integrates an AI Agent powered by Gemini (Firebase AI) that can execute actions in the app via Function Calling. Instead of modifying the UI or database directly, the AI dispatches MVI Intents — exactly as if the user had tapped a button.
User question ──▶ AskAiAgentUseCase (Gemini + Tool declarations)
│
┌─────────┴─────────┐
▼ ▼
Text response FunctionCall response
(show to user) │
▼
onAIFunctionCalled()
│
▼
ActionRegistry.processAIToolCall()
│
┌─────────────┼─────────────┐
▼ ▼ ▼
SAFE MODERATE CRITICAL
(execute) (execute + (pause, ask
snackbar) user to confirm)
| Level | Behavior | Example |
|---|---|---|
SAFE |
Executes immediately, no feedback | SearchTool → search events |
MODERATE |
Executes immediately + snackbar notification | ToggleGlobalAlarmsTool → toggle alarms |
CRITICAL |
Pauses, saves intent in pendingAIAction, requires explicit user confirmation |
CreateEventTool → create calendar event |
| Component | Role |
|---|---|
AITool |
Interface — each tool maps an LLM function call to an EventIntent |
ActionRegistry |
Singleton — discovers tools, dispatches function calls |
AskAiAgentUseCase |
Sends the prompt + tool declarations to Gemini, returns Text or FunctionCall |
RiskLevel |
Enum controlling execution policy (SAFE, MODERATE, CRITICAL) |
AIToolResult |
Sealed class wrapping dispatch results (Success, ToolNotFound, InvalidArguments) |
Step 1 — Create a class implementing AITool in core/.../ai/tools/:
class MyNewTool @Inject constructor() : AITool {
override val name = "my_new_action"
override val description = "Describe when the LLM should call this tool."
override val riskLevel = RiskLevel.SAFE // or MODERATE / CRITICAL
override val parametersSchema = mapOf(
"type" to "object",
"properties" to mapOf(
"param1" to mapOf("type" to "string", "description" to "…"),
),
"required" to listOf("param1"),
)
override fun parseArguments(args: Map<String, Any?>): EventIntent? {
val value = args["param1"]?.toString() ?: return null
return EventIntent.SomeExistingIntent(value) // reuse an MVI intent
}
}Step 2 — Register it in AIToolsModule (core/.../ai/di/):
@Provides @IntoSet @Singleton
fun provideMyNewTool(): AITool = MyNewTool()Step 3 — Done. ActionRegistry discovers it automatically via Hilt multibinding, includes it in the Gemini tool declarations, and routes function calls through onAIFunctionCalled().
Tip: Choose
RiskLevelcarefully —SAFEexecutes silently,MODERATEshows a snackbar, andCRITICALpauses for user confirmation.
- Android Studio Jellyfish+
- JDK 21
- Android SDK 36 (Compile/Target)
- Clone the repository:
git clone https://github.com/tonimadev/kairos-android-app.git
- Open in Android Studio and wait for Gradle synchronization.
- To run via CLI:
- Phone:
./gradlew :app:installDebug - Wear OS:
./gradlew :wear:installDebug
- Phone:
- Run all unit tests:
./gradlew testDebugUnitTest - Generate coverage report (Jacoco):
./gradlew createJacocoMergedCoverageReport - Apply code style:
./gradlew spotlessApply
For Release builds or full Firebase/AdMob integration, it is necessary:
- Add
google-services.jsoninapp/andwear/folders. - Configure signing keys and ad IDs in
local.propertiesor environment variables. Seebuild.gradle.ktsfor details on expected properties.
Contributions are welcome! Make sure to run ./gradlew spotlessApply before opening a Pull Request.
Developed by tonimadev




