README UrbaFix

UrbaFix - Application Mobile Kotlin

Application Android native pour la gestion des signalements citoyens auprès des mairies. Version 100% Kotlin avec Material Design 3, architecture offline-first et synchronisation différée.

📱 Objectif

UrbaFix permet aux citoyens de signaler des incidents dans leur quartier (nids de poule, déchets, éclairage défectueux, etc.) même sans connexion réseau. Les signalements sont stockés localement et synchronisés automatiquement avec le serveur dès que le réseau est disponible.

Cas d’usage principal : Fonctionnement dans les zones blanches (zones sans couverture réseau) avec synchronisation différée.

🎯 Fonctionnalités

✅ Implémentées

  1. Écran d’accueil
  2. Profil utilisateur
  3. Synchronisation des données
  4. Architecture Material Design 3
  5. Signaler un incident

🚧 En développement

🏗️ Architecture technique

Stack technologique

Structure du projet

app/src/main/java/com/example/monquartierkotlin/
├── data/
│   ├── local/
│   │   ├── dao/                    # Data Access Objects (Room)
│   │   │   ├── IncidentDao.kt
│   │   │   └── PhotoDao.kt
│   │   ├── entities/               # Entités de base de données
│   │   │   ├── IncidentEntity.kt   # Table incidents
│   │   │   └── PhotoEntity.kt      # Table photos
│   │   ├── AppDatabase.kt          # Configuration Room
│   │   └── Converters.kt           # Convertisseurs de types
│   ├── remote/
│   │   ├── dto/                    # Data Transfer Objects (API)
│   │   │   ├── ApiResponses.kt     # DTOs incidents/types/mairie
│   │   │   ├── ProfileDto.kt       # DTOs profil citoyen
│   │   │   └── SubmitIncidentRequest.kt
│   │   ├── UrbafixApi.kt       # Interface Retrofit
│   │   └── RetrofitClient.kt       # Configuration HTTP
│   └── repository/
│       └── IncidentRepository.kt   # Couche d'accès aux données
├── ui/
│   ├── screens/
│   │   ├── home/                   # Écran d'accueil
│   │   │   ├── HomeScreen.kt       # Carte + Liste + Groupes
│   │   │   ├── HomeViewModel.kt
│   │   │   ├── HomeViewModelFactory.kt
│   │   │   ├── IncidentDetailScreen.kt      # Vue détaillée complète
│   │   │   └── IncidentDetailViewModel.kt   # Chargement photos
│   │   └── profile/                # Écran profil
│   │       ├── ProfileScreen.kt    # + Partage app
│   │       └── ProfileViewModel.kt
│   ├── components/                 # Composants réutilisables
│   │   ├── IncidentCard.kt         # Carte de signalement
│   │   ├── IncidentGroupCard.kt    # Carte de groupe empilée
│   │   └── IncidentDetailSheet.kt  # Bottom sheet simple
│   └── theme/                      # Thème Material Design 3
├── domain/
│   └── model/
│       └── IncidentGroup.kt        # Modèle de groupe d'incidents
├── workers/
│   └── SyncIncidentsWorker.kt      # Synchronisation en arrière-plan
├── utils/
│   ├── DeviceIdManager.kt          # Gestion Device ID unique
│   ├── GeoUtils.kt                 # Calcul distances Haversine + groupement
│   └── MarkerUtils.kt              # Génération marqueurs avec badges
├── UrbafixApplication.kt       # Application class
└── MainActivity.kt                 # Activité principale

Modèle de données

IncidentEntity (Table Room)

data class IncidentEntity(
    val id: Long,                    // ID local
    val serverId: Long?,             // ID sur le serveur
    val mairieId: Long,
    val typeId: Long,
    val typeName: String,
    val typeColor: String,           // Hex color
    val adresse: String,
    val latitude: Double,
    val longitude: Double,
    val description: String,
    val statut: String,              // nouveau, en_cours, resolu, ferme
    val syncStatus: SyncStatus,      // PENDING, SYNCING, SYNCED, ERROR
    val createdAt: Long,             // Timestamp
    val reponseMairie: String?,      // Réponse de la mairie
    val dateReponse: Long?,          // Date de la réponse
    val deviceId: String             // ID unique de l'appareil
)

États de synchronisation

enum class SyncStatus {
    PENDING,    // En attente d'envoi
    SYNCING,    // En cours d'envoi
    SYNCED,     // Synchronisé avec succès
    ERROR       // Erreur lors de l'envoi
}

🌐 API Backend

L’application se connecte à l’API REST hébergée sur https://urbafix.fr

Endpoints utilisés

Incidents

GET /api/get_incidents.php?code_postal=06500&limit=100

Récupère tous les incidents d’un code postal

GET /api/get_types.php?code_postal=06500

Récupère les types d’incidents disponibles pour une mairie

POST /api/submit_incident.php
Content-Type: multipart/form-data

type_id, description, latitude, longitude, adresse, mairie_id, photos[]

Soumet un nouvel incident

Profil utilisateur

POST /api/register_device.php
Content-Type: application/x-www-form-urlencoded

device_id, code_postal, fcm_token (optionnel)

Enregistre un nouvel appareil

GET /api/get_profile.php?device_id=xxx&code_postal=06500

Récupère le profil d’un citoyen

POST /api/update_profile.php
Content-Type: application/json

{
  "device_id": "xxx",
  "nom": "Dupont",
  "prenom": "Jean",
  "email": "jean@example.com",
  "telephone": "0612345678",
  "code_postal": "06500"
}

Met à jour le profil utilisateur

Format des réponses

Toutes les réponses suivent ce format :

{
  "success": true|false,
  "message": "Message explicatif",
  "data": { ... }
}

Exemple de réponse d’incidents :

{
  "success": true,
  "count": 12,
  "mairie": {
    "id": 1,
    "nom": "Mairie de Menton",
    "code_postal": "06500"
  },
  "incidents": [
    {
      "id": 22,
      "type_id": 3,
      "type_nom": "Déchets",
      "type_couleur": "#10B981",
      "adresse": "Cours Georges V, 06500, Menton",
      "latitude": "43.77378910",
      "longitude": "7.49685980",
      "description": "Matelas déposé...",
      "statut": "resolu",
      "created_at": "2025-11-19 14:43:58",
      "photos": ["/uploads/incidents/22_1763559838_0.jpeg"],
      "nb_photos": 1
    }
  ]
}

🔄 Synchronisation offline-first

Principe

  1. Lecture : Les incidents sont d’abord lus depuis la base de données locale (Room)
  2. Synchronisation : Au démarrage et périodiquement, l’app télécharge les nouveaux incidents depuis l’API
  3. Création : Les nouveaux signalements sont d’abord sauvegardés localement avec syncStatus = PENDING
  4. Envoi : WorkManager tente d’envoyer les incidents PENDING toutes les 15 minutes quand le réseau est disponible
  5. Mise à jour : Une fois envoyés, les incidents passent à syncStatus = SYNCED et reçoivent leur serverId

WorkManager - Synchronisation en arrière-plan

// Configuration (toutes les 15 minutes, uniquement avec réseau)
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .build()

val syncRequest = PeriodicWorkRequestBuilder<SyncIncidentsWorker>(
    15, TimeUnit.MINUTES
)
    .setConstraints(constraints)
    .build()

🗺️ Intégration OpenStreetMap

Utilisation de osmdroid au lieu de Google Maps pour : - ✅ Aucune clé API nécessaire - ✅ Pas de restrictions d’utilisation - ✅ Données OpenStreetMap libres - ✅ Cohérence avec la version web

Configuration

MapView(context).apply {
    setTileSource(TileSourceFactory.MAPNIK)
    setMultiTouchControls(true)
    controller.setZoom(13.0)
    controller.setCenter(GeoPoint(43.7737, 7.4951)) // Menton
}

Permissions requises

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

🎨 Interface utilisateur

4 onglets dans la Bottom Navigation : 1. Accueil 🏠 : Carte + Liste des signalements 2. Signaler ➕ : Créer un nouveau signalement (à venir) 3. Mes signalements 📋 : Mes incidents (à venir) 4. Profil 👤 : Gestion du profil utilisateur

Écran d’accueil

┌─────────────────────────────────┐
│      UrbaFix                │
├─────────────────────────────────┤
│                                 │
│     [Carte OpenStreetMap]       │  40% de la hauteur
│     🔴 (3) ← Groupe 3 incidents │  Marqueurs avec badges
│     🟢      ← Incident seul     │
│                                 │
├─────────────────────────────────┤
│  ┌──────────────────┐           │
│  │📍 Nid de poule   │  [3]      │  Carte empilée = groupe
│  │15 Ave Boyer      │           │
│  └──────────────────┘           │  60% hauteur (scrollable)
│  ───────────────────────────    │
│  🗑️ Déchets                     │  Carte simple = individuel
│  Cours Georges V • Résolu       │
│  ───────────────────────────    │
│  💡 Éclairage défectueux        │
│  Rue Partouneaux • En cours     │
└─────────────────────────────────┘

Clic sur groupe → IncidentDetailScreen :
┌─────────────────────────────────┐
│ ← Détail de l'incident          │
├─────────────────────────────────┤
│   [Mini carte OSM centrée]      │
│                                 │
├─────────────────────────────────┤
│ ⭐ Incident principal            │
│    3 incidents dans cette zone  │
├─────────────────────────────────┤
│ 📌 Type: Nid de poule           │
│ 📝 Description: ...             │
│ 📍 Adresse: ...                 │
│ 📅 Date: 28 déc. à 14:30        │
│ ℹ️  Statut: Nouveau             │
├─────────────────────────────────┤
│ Photos (5) • De tous incidents  │
│ [🖼️] [🖼️] [🖼️] [🖼️] [🖼️] ──→   │
├─────────────────────────────────┤
│ 📋 Autres incidents (2)         │
│ • Nid de poule | il y a 2h      │
│   Description...                │
│ ───────────────────────────     │
│ • Nid de poule | il y a 1 jour  │
│   Description...                │
└─────────────────────────────────┘

Codes couleur des types

Indicateurs visuels

📦 Build et installation

Prérequis

Compilation

cd /home/franck/AndroidStudioProjects/UrbafixKotlin

# Build debug APK
./gradlew clean assembleDebug

# APK généré dans :
# app/build/outputs/apk/debug/app-debug.apk

Installation

# Via ADB
adb install -r app/build/outputs/apk/debug/app-debug.apk

# Taille de l'APK : ~17 MB

Configuration

Pour changer l’URL de l’API (dev/prod), éditer :

// app/src/main/java/.../data/remote/RetrofitClient.kt
private const val BASE_URL = "https://urbafix.fr/"

// Pour développement local :
// private const val BASE_URL = "http://10.0.2.2:8080/" // Émulateur
// private const val BASE_URL = "http://192.168.1.X:8080/" // Appareil physique

Code postal par défaut

// app/src/main/java/.../data/repository/IncidentRepository.kt
// app/src/main/java/.../ui/screens/profile/ProfileViewModel.kt
private const val DEFAULT_CODE_POSTAL = "06500" // Menton

🔒 Sécurité (Conforme ANSSI)

Implémentations de sécurité

1. Chiffrement de la base de données (SQLCipher)

// AppDatabase.kt
val passphrase = FingerprintManager.generateFingerprint(context).toByteArray()
val factory = SupportFactory(passphrase)

2. Certificate Pinning (Protection MITM)

// RetrofitClient.kt
val certificatePinner = CertificatePinner.Builder()
    .add("urbafix.fr", "sha256/PU4bTrzvP9A7Ft6KDCL+E90q/7+CT/H9oB0iwTy6iS8=")
    .build()

3. Blocage HTTP en clair

<!-- network_security_config.xml -->
<base-config cleartextTrafficPermitted="false">

4. Logs conditionnels

level = if (BuildConfig.DEBUG) {
    HttpLoggingInterceptor.Level.BODY
} else {
    HttpLoggingInterceptor.Level.NONE
}

5. Floutage automatique des visages (RGPD)

// ImageCompressor.kt
val anonymizedBitmap = FaceBlurrer.blurFaces(resizedBitmap, usePixelation = false)
if (anonymizedBitmap == null) {
    // En cas d'erreur de floutage, on rejette l'image (sécurité RGPD)
    return null
}

Conformité ANSSI

Renouvellement du certificat

# Obtenir le nouveau pin
echo | openssl s_client -connect urbafix.fr:443 -servername urbafix.fr \
  | openssl x509 -pubkey -noout \
  | openssl pkey -pubin -outform der \
  | openssl dgst -sha256 -binary | base64

# Mettre à jour :
# - RetrofitClient.kt ligne 72
# - network_security_config.xml ligne 17

🔧 Dépendances principales

dependencies {
    // Compose BOM
    implementation(platform("androidx.compose:compose-bom:2024.02.00"))
    implementation("androidx.compose.material3:material3")
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-tooling-preview")

    // Room + SQLCipher
    implementation("androidx.room:room-runtime:2.6.1")
    implementation("androidx.room:room-ktx:2.6.1")
    ksp("androidx.room:room-compiler:2.6.1")
    implementation("net.zetetic:android-database-sqlcipher:4.5.4@aar")
    implementation("androidx.sqlite:sqlite:2.4.0")

    // Retrofit + Gson + Certificate Pinning
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")

    // WorkManager
    implementation("androidx.work:work-runtime-ktx:2.9.0")

    // OpenStreetMap
    implementation("org.osmdroid:osmdroid-android:6.1.18")

    // DataStore
    implementation("androidx.datastore:datastore-preferences:1.0.0")

    // Coil (images)
    implementation("io.coil-kt:coil-compose:2.4.0")

    // ML Kit Face Detection (RGPD)
    implementation("com.google.mlkit:face-detection:16.1.6")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
}

🐛 Debug et logs

Activer les logs détaillés

# Filtrer les logs de l'application
adb logcat | grep -E "(IncidentRepository|HomeViewModel|ProfileViewModel)"

# Voir les logs de synchronisation
adb logcat | grep -E "(SyncIncidentsWorker|WorkManager)"

# Logs HTTP (Retrofit)
adb logcat | grep -E "(OkHttp)"

Vérifier la base de données Room

# Via ADB shell
adb shell
run-as com.example.monquartierkotlin
cd databases
sqlite3 incidents_database

# Requêtes utiles
SELECT * FROM incidents;
SELECT * FROM incidents WHERE syncStatus = 'PENDING';
SELECT COUNT(*) FROM incidents;

📊 Fonctionnement détaillé

Flux de chargement des incidents

Démarrage app
    ↓
HomeViewModel.init()
    ↓
syncIncidents() appelé
    ↓
IncidentRepository.syncIncidentsFromApi()
    ↓
GET https://urbafix.fr/api/get_incidents.php?code_postal=06500
    ↓
Réponse JSON avec 12 incidents
    ↓
Conversion DTO → Entity
    ↓
Insertion/Update dans Room Database
    ↓
Flow<List<IncidentEntity>> mis à jour
    ↓
HomeScreen recompose automatiquement
    ↓
Affichage carte + liste

Flux de création d’un profil

ProfileScreen affiché
    ↓
ProfileViewModel.loadProfile()
    ↓
DeviceIdManager.getDeviceId(context)
    ↓
GET https://urbafix.fr/api/get_profile.php?device_id=xxx
    ↓
Si profil n'existe pas : registerDevice()
    ↓
POST https://urbafix.fr/api/register_device.php
    ↓
Profil créé (anonyme par défaut)
    ↓
Utilisateur remplit le formulaire
    ↓
ProfileViewModel.updateProfile(nom, prenom, email, tel)
    ↓
POST https://urbafix.fr/api/update_profile.php
    ↓
Profil mis à jour, isAnonymous = false

🚀 Évolutions futures

Fonctionnalités à implémenter

  1. Formulaire de signalement
  2. Mes signalements
  3. Notifications
  4. Améliorations carte
  5. Gestion hors-ligne
  6. Partage et engagement

📝 Notes de développement

Différences avec la version WebView

L'application UrbaFix (WebView) charge index.html dans un WebView Android. L'application UrbafixKotlin est 100% native avec : - ✅ Meilleures performances - ✅ Intégration native Android (permissions, caméra, GPS) - ✅ Offline-first réel avec Room - ✅ Synchronisation robuste avec WorkManager - ✅ Architecture testable et maintenable - ✅ Expérience utilisateur Material Design 3

Choix techniques

Pourquoi Jetpack Compose ? - UI déclarative moderne - Moins de code boilerplate - Hot reload - Intégration parfaite avec Material Design 3

Pourquoi Room ? - ORM officiel Android - Support des Flow pour la réactivité - Compile-time verification des requêtes SQL - Migration automatique

Pourquoi WorkManager ? - Garantit l’exécution en arrière-plan - Respect des contraintes système (batterie, réseau) - Retry automatique en cas d’échec - Persistent même après redémarrage

Pourquoi osmdroid ? - Pas de clé API requise - Pas de quotas - Cohérence avec la version web - Données libres OpenStreetMap

👥 Contributeurs

📄 Licence

Application propriétaire pour la gestion des signalements citoyens.


Version : 1.5.0 Dernière mise à jour : 03 janvier 2026 API Backend : https://urbafix.fr Changelog v1.5.0 : - ✅ Support des vidéos de 5 secondes max (capture + galerie) - ✅ Avertissement RGPD obligatoire avant capture vidéo - ✅ Compression et troncature automatique des vidéos - ✅ Aperçu vidéos avec indicateurs visuels (▶, durée, bordure bleue) - ✅ Upload vidéo en Base64 (compatible infrastructure existante) - ⚠️ Floutage visages non implémenté pour vidéos (mitigation : dialogue RGPD)