Loading repository data…
Loading repository data…
tonimadev / repository
Kairós is a modern Android application designed to ensure you never miss a calendar event again. It intelligently syncs with your device's calendar and turns your appointments into unmissable, full-screen alarms, similar to a native alarm clock.
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.
This project follows modern Android development principles with MVVM + MVI architecture.
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 ──▶ DB (ChatHistoryDao) ──▶ AskAiAgentUseCase (Gemini + Tool declarations)
│
┌─────────────────┴─────────────────┐
▼ ▼
Text response FunctionCall response
(save to DB & show) │
▼
onAIFunctionCalled()
│
▼
ActionRegistry.processAIToolCall()
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
SAFE MODERATE CRITICAL
(execute) (execute + snackbar) (pause, ask user)
│ │
└─────────────────────┘
│
DB (Save FunctionResponse)
│
(Loop back to AskAiAgentUseCase)
| 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 |
|---|---|
ChatHistoryDao | Room database interface managing the offline chat history persistence. |
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 (model.startChat), 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.
git clone https://github.com/tonimadev/kairos-android-app.git
./gradlew :app:installDebug./gradlew :wear:installDebug./gradlew testDebugUnitTest./gradlew createJacocoMergedCoverageReport./gradlew spotlessApplyFor Release builds or full Firebase/AdMob integration, it is necessary:
google-services.json in app/ and wear/ folders.local.properties or environment variables. See build.gradle.kts for details on expected properties.Contributions are welcome! Make sure to run ./gradlew spotlessApply before opening a Pull Request.
Developed by tonimadev