Files
simple-notes-sync/docs/DOCS.de.md
inventory69 67b226a5c3 feat(v1.5.0): icons, batch delete toast, cursor fix, docs refactor
FEATURES
========

Batch Delete Toast Aggregation:
- New deleteMultipleNotesFromServer() method
- Shows single aggregated toast instead of multiple ("3 notes deleted from server")
- Partial success handling ("3 of 5 notes deleted from server")
- Added string resources: snackbar_notes_deleted_from_server, snackbar_notes_deleted_from_server_partial

Text Editor Cursor Fix:
- Fixed cursor jumping to end after every keystroke when editing notes
- Added initialCursorSet flag to only set cursor position on first load
- Cursor now stays at user's position while editing
- Changed LaunchedEffect(content) to LaunchedEffect(Unit) to prevent repeated resets

DOCUMENTATION REFACTOR
======================

Breaking Change: English is now the default language
- README.md: Now English (was German)
- QUICKSTART.md: Now English (was German)
- CHANGELOG.md: Now English (was mixed EN/DE)
- docs/*.md: All English (was German)
- German versions: Use .de.md suffix (README.de.md, QUICKSTART.de.md, etc.)

Updated for v1.5.0:
- CHANGELOG.md: Fully translated to English with v1.5.0 release notes
- CHANGELOG.de.md: Created German version
- FEATURES.md: Added i18n section, Selection Mode, Jetpack Compose updates
- FEATURES.de.md: Updated with v1.5.0 features
- UPCOMING.md: v1.5.0 marked as released, v1.6.0/v1.7.0 roadmap
- UPCOMING.de.md: Updated German version

All language headers updated:
- English: [Deutsch](*.de.md) · **English**
- German: **Deutsch** · [English](*.md)

F-DROID METADATA
================

Changelogs (F-Droid):
- fastlane/metadata/android/en-US/changelogs/13.txt: Created
- fastlane/metadata/android/de-DE/changelogs/13.txt: Created

Descriptions:
- full_description.txt (EN/DE): Updated with v1.5.0 changes
  - Selection Mode instead of Swipe-to-Delete
  - i18n support highlighted
  - Jetpack Compose UI mentioned
  - Silent-Sync Mode added

OTHER FIXES
===========

Code Quality:
- Unused imports removed from multiple files
- maxLineLength fixes
- Detekt config optimized (increased thresholds for v1.5.0)
- AboutScreen: Uses app foreground icon directly
- EmptyState: Shows app icon instead of emoji
- themes.xml: Splash screen uses app foreground icon
2026-01-16 16:31:30 +01:00

14 KiB
Raw Blame History

Simple Notes Sync - Technische Dokumentation

Diese Datei enthält detaillierte technische Informationen über die Implementierung, Architektur und erweiterte Funktionen.

🌍 Sprachen: Deutsch · English


📐 Architektur

Gesamtübersicht

┌─────────────────┐
│  Android App    │
│  (Kotlin)       │
└────────┬────────┘
         │ WebDAV/HTTP
         │
┌────────▼────────┐
│  WebDAV Server  │
│  (Docker)       │
└─────────────────┘

Android App Architektur

app/
├── models/
│   ├── Note.kt              # Data class für Notizen
│   └── SyncStatus.kt        # Sync-Status Enum
├── storage/
│   └── NotesStorage.kt      # Lokale JSON-Datei Speicherung
├── sync/
│   ├── WebDavSyncService.kt # WebDAV Sync-Logik
│   ├── NetworkMonitor.kt    # WLAN-Erkennung
│   ├── SyncWorker.kt        # WorkManager Background Worker
│   └── BootReceiver.kt      # Device Reboot Handler
├── adapters/
│   └── NotesAdapter.kt      # RecyclerView Adapter
├── utils/
│   ├── Constants.kt         # App-Konstanten
│   ├── NotificationHelper.kt# Notification Management
│   └── Logger.kt            # Debug/Release Logging
└── activities/
    ├── MainActivity.kt      # Hauptansicht mit Liste
    ├── NoteEditorActivity.kt# Editor für Notizen
    └── SettingsActivity.kt  # Server-Konfiguration

🔄 Auto-Sync Implementierung

WorkManager Periodic Task

Der Auto-Sync basiert auf WorkManager mit folgender Konfiguration:

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.UNMETERED)  // Nur WiFi
    .build()

val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
    30, TimeUnit.MINUTES,  // Alle 30 Minuten
    10, TimeUnit.MINUTES   // Flex interval
)
    .setConstraints(constraints)
    .build()

Warum WorkManager?

  • Läuft auch wenn App geschlossen ist
  • Automatischer Restart nach Device Reboot
  • Battery-efficient (Android managed)
  • Garantierte Ausführung bei erfüllten Constraints

Network Detection

Wir verwenden Gateway IP Comparison um zu prüfen, ob der Server erreichbar ist:

fun isInHomeNetwork(): Boolean {
    val gatewayIP = getGatewayIP()         // z.B. 192.168.0.1
    val serverIP = extractIPFromUrl(serverUrl)  // z.B. 192.168.0.188
    
    return isSameNetwork(gatewayIP, serverIP)  // Prüft /24 Netzwerk
}

Vorteile:

  • Keine Location Permissions nötig
  • Funktioniert mit allen Android Versionen
  • Zuverlässig und schnell

Sync Flow

1. WorkManager wacht auf (alle 30 Min)
   ↓
2. Check: WiFi connected?
   ↓
3. Check: Same network as server?
   ↓
4. Load local notes
   ↓
5. Upload neue/geänderte Notes → Server
   ↓
6. Download remote notes ← Server
   ↓
7. Merge & resolve conflicts
   ↓
8. Update local storage
   ↓
9. Show notification (if changes)

<EFBFBD> Sync-Trigger Übersicht

Die App verwendet 4 verschiedene Sync-Trigger mit unterschiedlichen Anwendungsfällen:

Trigger Datei Funktion Wann? Pre-Check?
1. Manueller Sync MainActivity.kt triggerManualSync() User klickt auf Sync-Button im Menü Ja
2. Auto-Sync (onResume) MainActivity.kt triggerAutoSync() App wird geöffnet/fortgesetzt Ja
3. Hintergrund-Sync (Periodic) SyncWorker.kt doWork() Alle 15/30/60 Minuten (konfigurierbar) Ja
4. WiFi-Connect Sync NetworkMonitor.ktSyncWorker.kt triggerWifiConnectSync() WiFi verbunden Ja

Server-Erreichbarkeits-Check (Pre-Check)

Alle 4 Sync-Trigger verwenden vor dem eigentlichen Sync einen Pre-Check:

// WebDavSyncService.kt - isServerReachable()
suspend fun isServerReachable(): Boolean = withContext(Dispatchers.IO) {
    return@withContext try {
        Socket().use { socket ->
            socket.connect(InetSocketAddress(host, port), 2000)  // 2s Timeout
        }
        true
    } catch (e: Exception) {
        Logger.d(TAG, "Server not reachable: ${e.message}")
        false
    }
}

Warum Socket-Check statt HTTP-Request?

  • Schneller: Socket-Connect ist instant, HTTP-Request dauert länger
  • 🔋 Akkuschonender: Kein HTTP-Overhead (Headers, TLS Handshake, etc.)
  • 🎯 Präziser: Prüft nur Netzwerk-Erreichbarkeit, nicht Server-Logik
  • 🛡️ Verhindert Fehler: Erkennt fremde WiFi-Netze bevor Sync-Fehler entsteht

Wann schlägt der Check fehl?

  • Server offline/nicht erreichbar
  • Falsches WiFi-Netzwerk (z.B. öffentliches Café-WiFi)
  • Netzwerk noch nicht bereit (DHCP/Routing-Delay nach WiFi-Connect)
  • VPN blockiert Server-Zugriff
  • Keine WebDAV-Server-URL konfiguriert

Sync-Verhalten nach Trigger-Typ

Trigger Bei Server nicht erreichbar Bei erfolgreichem Sync Throttling
Manueller Sync Toast: "Server nicht erreichbar" Toast: " Gesynct: X Notizen" Keins
Auto-Sync (onResume) Silent abort (kein Toast) Toast: " Gesynct: X Notizen" Max. 1x/Min
Hintergrund-Sync Silent abort (kein Toast) Silent (LocalBroadcast only) 15/30/60 Min
WiFi-Connect Sync Silent abort (kein Toast) Silent (LocalBroadcast only) WiFi-basiert

🔋 Akku-Optimierung

Verbrauchsanalyse

Komponente Frequenz Verbrauch Details
WorkManager Wakeup Alle 30 Min ~0.15 mAh System wacht auf
Network Check 48x/Tag ~0.03 mAh Gateway IP check
WebDAV Sync 2-3x/Tag ~1.5 mAh Nur bei Änderungen
Total - ~12 mAh/Tag ~0.4% bei 3000mAh

Optimierungen

  1. IP Caching

    private var cachedServerIP: String? = null
    // DNS lookup nur 1x beim Start, nicht bei jedem Check
    
  2. Throttling

    private var lastSyncTime = 0L
    private const val MIN_SYNC_INTERVAL_MS = 60_000L  // Max 1 Sync/Min
    
  3. Conditional Logging

    object Logger {
        fun d(tag: String, msg: String) {
            if (BuildConfig.DEBUG) Log.d(tag, msg)
        }
    }
    
  4. Network Constraints

    • Nur WiFi (nicht mobile Daten)
    • Nur wenn Server erreichbar
    • Keine permanenten Listeners

📦 WebDAV Sync Details

Upload Flow

suspend fun uploadNotes(): Int {
    val localNotes = storage.loadAllNotes()
    var uploadedCount = 0
    
    for (note in localNotes) {
        if (note.syncStatus == SyncStatus.PENDING) {
            val jsonContent = note.toJson()
            val remotePath = "$serverUrl/${note.id}.json"
            
            sardine.put(remotePath, jsonContent.toByteArray())
            
            note.syncStatus = SyncStatus.SYNCED
            storage.saveNote(note)
            uploadedCount++
        }
    }
    
    return uploadedCount
}

Download Flow

suspend fun downloadNotes(): DownloadResult {
    val remoteFiles = sardine.list(serverUrl)
    var downloadedCount = 0
    var conflictCount = 0
    
    for (file in remoteFiles) {
        if (!file.name.endsWith(".json")) continue
        
        val content = sardine.get(file.href)
        val remoteNote = Note.fromJson(content)
        val localNote = storage.loadNote(remoteNote.id)
        
        if (localNote == null) {
            // Neue Note vom Server
            storage.saveNote(remoteNote)
            downloadedCount++
        } else if (localNote.modifiedAt < remoteNote.modifiedAt) {
            // Server hat neuere Version
            storage.saveNote(remoteNote)
            downloadedCount++
        } else if (localNote.modifiedAt > remoteNote.modifiedAt) {
            // Lokale Version ist neuer → Conflict
            resolveConflict(localNote, remoteNote)
            conflictCount++
        }
    }
    
    return DownloadResult(downloadedCount, conflictCount)
}

Conflict Resolution

Strategie: Last-Write-Wins mit Conflict Copy

fun resolveConflict(local: Note, remote: Note) {
    // Remote Note umbenennen (Conflict Copy)
    val conflictNote = remote.copy(
        id = "${remote.id}_conflict_${System.currentTimeMillis()}",
        title = "${remote.title} (Konflikt)"
    )
    
    storage.saveNote(conflictNote)
    
    // Lokale Note bleibt
    local.syncStatus = SyncStatus.SYNCED
    storage.saveNote(local)
}

🔔 Notifications

Notification Channels

val channel = NotificationChannel(
    "notes_sync_channel",
    "Notizen Synchronisierung",
    NotificationManager.IMPORTANCE_DEFAULT
)

Success Notification

fun showSyncSuccess(context: Context, count: Int) {
    val intent = Intent(context, MainActivity::class.java)
    val pendingIntent = PendingIntent.getActivity(context, 0, intent, FLAGS)
    
    val notification = NotificationCompat.Builder(context, CHANNEL_ID)
        .setContentTitle("Sync erfolgreich")
        .setContentText("$count Notizen synchronisiert")
        .setContentIntent(pendingIntent)  // Click öffnet App
        .setAutoCancel(true)              // Dismiss on click
        .build()
    
    notificationManager.notify(NOTIFICATION_ID, notification)
}

🛡️ Permissions

Die App benötigt minimale Permissions:

<!-- Netzwerk -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

<!-- Notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<!-- Boot Receiver -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<!-- Battery Optimization (optional) -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

Keine Location Permissions!
Wir verwenden Gateway IP Comparison statt SSID-Erkennung. Keine Standortberechtigung nötig.


🧪 Testing

Server testen

# WebDAV Server erreichbar?
curl -u noteuser:password http://192.168.0.188:8080/

# Datei hochladen
echo '{"test":"data"}' > test.json
curl -u noteuser:password -T test.json http://192.168.0.188:8080/test.json

# Datei herunterladen
curl -u noteuser:password http://192.168.0.188:8080/test.json

Android App testen

Unit Tests:

cd android
./gradlew test

Instrumented Tests:

./gradlew connectedAndroidTest

Manual Testing Checklist:

  • Notiz erstellen → in Liste sichtbar
  • Notiz bearbeiten → Änderungen gespeichert
  • Notiz löschen → aus Liste entfernt
  • Manueller Sync → Server Status "Erreichbar"
  • Auto-Sync → Notification nach ~30 Min
  • App schließen → Auto-Sync funktioniert weiter
  • Device Reboot → Auto-Sync startet automatisch
  • Server offline → Error Notification
  • Notification Click → App öffnet sich

🚀 Build & Deployment

Debug Build

cd android
./gradlew assembleDebug
# APK: app/build/outputs/apk/debug/app-debug.apk

Release Build

./gradlew assembleRelease
# APK: app/build/outputs/apk/release/app-release-unsigned.apk

Signieren (für Distribution)

# Keystore erstellen
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias

# APK signieren
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
  -keystore my-release-key.jks \
  app-release-unsigned.apk my-alias

# Optimieren
zipalign -v 4 app-release-unsigned.apk app-release.apk

🐛 Debugging

LogCat Filter

# Nur App-Logs
adb logcat -s SimpleNotesApp NetworkMonitor SyncWorker WebDavSyncService

# Mit Timestamps
adb logcat -v time -s SyncWorker

# In Datei speichern
adb logcat -s SyncWorker > sync_debug.log

Common Issues

Problem: Auto-Sync funktioniert nicht

Lösung: Akku-Optimierung deaktivieren
Settings → Apps → Simple Notes → Battery → Don't optimize

Problem: Server nicht erreichbar

Check: 
1. Server läuft? → docker-compose ps
2. IP korrekt? → ip addr show
3. Port offen? → telnet 192.168.0.188 8080
4. Firewall? → sudo ufw allow 8080

Problem: Notifications kommen nicht

Check:
1. Notification Permission erteilt?
2. Do Not Disturb aktiv?
3. App im Background? → Force stop & restart

📚 Dependencies

// Core
androidx.core:core-ktx:1.12.0
androidx.appcompat:appcompat:1.6.1
com.google.android.material:material:1.11.0

// Lifecycle
androidx.lifecycle:lifecycle-runtime-ktx:2.7.0

// RecyclerView
androidx.recyclerview:recyclerview:1.3.2

// Coroutines
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3

// WorkManager
androidx.work:work-runtime-ktx:2.9.0

// WebDAV Client
com.github.thegrizzlylabs:sardine-android:0.8

// Broadcast (deprecated but working)
androidx.localbroadcastmanager:localbroadcastmanager:1.1.0

🔮 Roadmap

v1.1

  • Suche & Filter
  • Dark Mode
  • Tags/Kategorien
  • Markdown Preview

v2.0

  • Desktop Client (Flutter)
  • End-to-End Verschlüsselung
  • Shared Notes (Collaboration)
  • Attachment Support

📖 Weitere Dokumentation


Letzte Aktualisierung: 25. Dezember 2025