Imported from lomicbourlotroche/DrawTaxi (
AGENTS.md). Install upstream withnpx skills add lomicbourlotroche/DrawTaxi. Copyright stays with the author.
Agent Guidelines for DrawTaxi
Project Overview
- Type: Android native application (Kotlin)
- UI Framework: Jetpack Compose
- Architecture: MVVM with Repository pattern
- Database: Room (v11)
- Min SDK: 27, Target SDK: 35, Compile SDK: 36
Build Commands
Gradle Wrapper
Always use ./gradlew (Unix/Mac) or gradlew.bat (Windows) from the project root.
Build Debug APK
./gradlew assembleDebug
# APK output: app/build/outputs/apk/debug/app-debug.apk
Build Release APK
./gradlew assembleRelease
# APK output: app/build/outputs/apk/release/app-release.apk
Clean Build
./gradlew clean
Run Tests
# All unit tests
./gradlew test
# Single test class
./gradlew test --tests "com.drawtaxi.app.logic.SmsParserTest"
# Single test method
./gradlew test --tests "com.drawtaxi.app.logic.SmsParserTest.testParseStandardSms"
Instrumentation Tests (on device/emulator)
./gradlew connectedAndroidTest
Lint Analysis
./gradlew lint
# Results: app/build/reports/lint-results.html
Build with Dependencies Refresh
./gradlew --refresh-dependencies assembleDebug
Code Style Guidelines
Kotlin Version & Configuration
- Kotlin Version: 2.3.21
- JVM Target: 17
- Compose Compiler: Kotlin compiler plugin (
org.jetbrains.kotlin.plugin.compose) - KSP Version: 2.3.6 (remplace kapt pour Room)
Package Structure
com.drawtaxi.app/
├── data/ # Data layer (Repository, Models, local storage)
│ └── local/ # Room database, DAOs, SettingsManager, SecureCredentialsManager
├── logic/ # Business logic (SmsParser, KolectoManager, etc.)
├── ui/ # Presentation layer
│ ├── components/ # Reusable Compose components
│ ├── screens/ # Screen composables
│ └── theme/ # Colors, Typography, Theme
└── car/ # Android Auto specific code
Import Organization
- Android/Kotlin standard library
- AndroidX libraries (core, lifecycle, compose, etc.)
- Third-party libraries (Room, Compose Material, etc.)
- Internal app imports (grouped by package)
Example from MainActivity.kt:
import android.Manifest
import android.content.Intent
import androidx.activity.compose.*
import androidx.compose.foundation.* // Wildcard for layout/foundation
import androidx.compose.material3.* // Material3 components
import androidx.compose.runtime.* // State, remember, LaunchedEffect
import androidx.compose.ui.*
import androidx.core.content.*
import androidx.lifecycle.viewmodel.compose.*
import com.drawtaxi.app.ui.*
import com.drawtaxi.app.data.*
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Classes | PascalCase | TaxiViewModel, RideRequest |
| Functions | camelCase | parseSms(), validateRide() |
| Properties | camelCase | brandColor, validatedRides |
| Constants | PascalCase | companion object members use camelCase |
| Composables | PascalCase | TaxiCard(), RideDetailScreen() |
| Enum values | PascalCase | SmsField.DE, SmsField.VERS |
| File names | Match class/function name | ParseSms.kt, TaxiCard.kt |
Composable Function Guidelines
- Modifier Parameter: Always include
modifier: Modifier = Modifieras first optional param - Preview Annotations: Use
@Preview(showBackground = true)for previews - State Management: Use
rememberfor local state,collectAsState()for flows - Experimental APIs: Add
@OptIn(ExperimentalMaterial3Api::class)when needed
Template:
@Composable
fun ComponentName(
requiredParam: String,
modifier: Modifier = Modifier,
optionalParam: String? = null
) {
// Implementation
}
@Preview(showBackground = true)
@Composable
fun ComponentNamePreview() {
ComponentName(requiredParam = "value")
}
Data Classes (Models)
- Use
data classfor immutable data models - Default values for optional fields
- Use
companion objectfor factory methods (e.g.,createStableId()) - Include profitability fields:
fuelCost,operatingCost,durationMinutes,profitabilityPercent
Example:
data class RideRequest(
val id: String,
val sender: String,
val body: String,
val departure: String = "",
val arrival: String = "",
val time: String = "",
val distanceKm: Double = 28.0,
val timestamp: Long = System.currentTimeMillis(),
val isPending: Boolean = true,
val date: String = "",
val price: Double = 0.0,
val fuelCost: Double = 0.0,
val operatingCost: Double = 0.0,
val durationMinutes: Int = 0,
val profitabilityPercent: Double = 0.0
) {
companion object {
fun createStableId(sender: String, body: String, timestamp: Long): String {
val raw = "$sender|$body|$timestamp"
return java.util.UUID.nameUUIDFromBytes(raw.toByteArray()).toString()
}
fun calculateProfitability(price: Double, fuelCost: Double, operatingCost: Double): Double {
val totalCost = fuelCost + operatingCost
if (totalCost == 0.0 || price == 0.0) return 0.0
return ((price - totalCost) / price) * 100.0
}
}
}
ViewModel Pattern
- Extend
ViewModelfor state holders - Expose state as
StateFlow - Use
ViewModelProvider.Factoryfor dependency injection - Wrap suspend operations in
viewModelScope.launch
Example:
class TaxiViewModel(private val repository: TaxiRepository) : ViewModel() {
val validatedRides: StateFlow<List<RideRequest>> = repository.validatedRides
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun validateRide(id: String) {
viewModelScope.launch {
repository.validateRide(id)
}
}
}
class TaxiViewModelFactory(private val repository: TaxiRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(TaxiViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return TaxiViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Room Database
- Use
@Databaseannotation with entities and version - Create DAOs with suspend functions for DB operations
- Singleton pattern with
getDatabase()factory method - Use
@Volatilefor INSTANCE field - Use
fallbackToDestructiveMigration()for schema changes - Current database version: 11
- Room compiler uses KSP (not kapt) — use
ksp(...)in dependencies
Coroutines & Flow
- Use
viewModelScope.launchfor ViewModel coroutines - Use
Dispatchers.IOfor I/O operations (network, DB) - Prefer
StateFlowoverLiveDatafor Compose
Error Handling
- Return
nullfor expected failure cases (e.g.,parseSms()returns null if parsing fails) - Throw
IllegalArgumentExceptionin factorycreate()methods for unknown types - Use
runCatchingortry-catchfor operations that may throw
Null Safety
- Prefer nullable types (
String?) over platform types - Use
?.and?.letfor safe navigation - Use Elvis operator
?:for default values - Avoid
!!operator except in rare cases where null is provably impossible
Testing
- Unit tests go in
app/src/test/java/... - Use JUnit 4 with
@Testannotation - Group related tests in a test class
- Use descriptive test names:
testParse<Scenario>()orshould<Behavior>()
Example:
class SmsParserTest {
@Test
fun testParseStandardSms() {
val ride = SmsParser.parse("+33612345678", "Taxi depuis Paris vers Lyon à 14h30")
assertNotNull(ride)
assertEquals("Paris", ride?.departure)
}
}
UI Design Guidelines
Color Palette (Tailwind-inspired)
- Primary: Indigo500 (
#6366F1) - Success: Emerald500 (
#10B981) - Error: Rose500 (
#F43F5E) - Warning: Amber500 (
#F59E0B) - Slate: Slate50 to Slate950 for neutrals
Card Style
- Rounded corners:
RoundedCornerShape(20.dp)for cards,16.dpfor smaller cards - Shadow elevation: 4dp with colored shadow
- Icon containers:
RoundedCornerShape(10.dp)withbrandColor.copy(alpha = 0.1f)background - Gradient headers for hero sections
Typography
- Headlines:
headlineMedium/headlineSmallwithFontWeight.Bold - Titles:
titleMediumwithFontWeight.Bold - Body:
bodyMediumfor regular text - Labels:
labelSmall/labelMediumfor metadata
Spacing
- Horizontal padding:
16.dpfor content,20.dpfor section headers - Vertical spacing:
6.dpbetween cards,12.dpbetween sections - Bottom nav padding: accounts for
100.dpspacer at bottom of scrollable screens
Key Features
SMS Parsing (ParseSms.kt) - Optimisé
- Détection Noms/Prénoms : "Bonjour Jean", "Je m'appelle..."
- Heures Intelligentes :
14h,14h30,14:30,dans 30min,dans 1h - Dates Avancées : Jours de la semaine ("lundi" → date exacte), "demain", "après-demain", formats textuels ("15 jan")
- Vocabulaire Étendu : "rdv à", "direction", "prendre rue", "jusqu'à"
- Context-aware analysis (greeting, politeness, urgency detection)
- Confidence scoring per field and overall
- Deduplication cache (30s window)
- Intent detection: confirmation, cancellation, modification
- Missing field extraction for auto-replies
SMS Reception
- SmsReceiver: BroadcastReceiver with deduplication
- SmsWatcher: ContentObserver with 2s debounce
- SmsForegroundService: Foreground service with polling 10s + ContentObserver
- All three work together for reliable SMS capture
Kolecto Integration
- Invoice screen with completed rides list and filters
- Manual invoice creation data for Kolecto web platform
- Ride details with HT/TVA/TTC breakdown
- Direct link to Kolecto web app
- Toggle in settings + invoices tab in bottom navigation
Profitability
- Auto-calculated on ride completion based on:
fuelCost = distanceKm × fuelCostPerKmoperatingCost = (durationMinutes / 60) × operatingCostPerHourprofitabilityPercent = ((price - totalCost) / price) × 100
- Displayed in Stats, Accounting, RideDetail, and RideCompletion screens
- Affiché uniquement dans QuoteScreen avant envoi du devis
GPS Navigation (GpsNavigationScreen.kt)
- MapLibre Native map with OpenFreeMap tiles
- LocationManager for real-time position
- MapLibre Navigation SDK for turn-by-turn guidance (core + ui-android)
- ETA, distance, speed display
- Start/stop navigation controls
Common Operations
Add a New Screen
- Create file in
ui/screens/ - Add
@Composablefunction with parameters for navigation state and callbacks - Register in Navigation (in MainActivity or NavHost)
- Add bottom nav item if needed
Add a New Data Model
- Add data class in
data/Models.ktor create new file indata/ - If persistence needed, create Room Entity in
data/local/RideEntity.kt - Add DAO methods in
data/local/RideDao.kt - Update repository with CRUD operations
- Increment database version in
AppDatabase.kt
Add a New Setting
- Add field to
AppSettingsdata class with default value - Add key in
SettingsManager.Keysobject - Update
settingsFlowmapping inSettingsManager - Update
updateSettings()to persist the value - Update settings UI screens to display/edit
Add a New Logic Module
- Create file in
logic/ - Use
objectfor singletons (e.g.,KolectoManager) - Use
Contextparameter for Android APIs - Log with
android.util.Logusing aTAGconstant
Important Notes
- NO PDF generation: Invoices were replaced by text receipts + manual Kolecto entry
- Database version is 11: Any schema change requires incrementing this
- SMS permissions are critical: App won't work without RECEIVE_SMS, READ_SMS, SEND_SMS
- Foreground service: SmsForegroundService auto-restarts on task removal
- Profitability defaults: fuelCostPerKm = 0.12, operatingCostPerHour = 15.0
- Kolecto: No API integration - use InvoiceScreen to prepare data for manual entry on Kolecto web
- MapLibre:
org.maplibre.gl:android-sdk:11.12.1pour l'affichage,org.maplibre.navigation:navigation-core:5.0.0-pre12pour le guidage turn-by-turn - ⚠️ Navigation SDK:
5.0.0-pre12est la seule version disponible (toutes sont des pre-releases). Verrouillée dansbuild.gradle.ktsviaconstraintspour éviter les breaking changes automatiques. Surveiller les releases stables sur https://github.com/maplibre/maplibre-navigation-android/releases - Tuiles: OpenStreetMap raster (
https://a.tile.openstreetmap.org/{z}/{x}/{y}.png) — gratuit, sans clé API - KSP remplace kapt : le plugin
com.google.devtools.kspversion2.3.6est utilisé pour Room - TVA Unique: 10% sur le total HT (transport + attente)
- Sécurité: Credentials OVH chiffrés via
EncryptedSharedPreferences - Timeout IA: 60s sur l'inférence Llama
- Exemption Batterie: Demandée automatiquement au démarrage