🐛 Release v1.1.1 - Critical Bugfixes

 Server-Erreichbarkeits-Check vor jedem Sync
- Socket-Check mit 2s Timeout (DHCP/Routing-Init abwarten)
- Verhindert Fehler-Notifications in fremden WiFi-Netzen
- Verhindert Fehler bei Netzwerk-Initialisierung (WiFi-Connect)
- Stiller Abbruch wenn Server nicht erreichbar
- 80% schnellerer Abbruch: 2s statt 10+ Sekunden

🔧 Notification-Verbesserungen
- Alte Notifications werden beim App-Start gelöscht
- Fehler-Notifications verschwinden automatisch nach 30s
- Bessere Batterie-Effizienz

📱 UI-Bugfixes
- Sync-Icon nur anzeigen wenn Sync konfiguriert ist
- Swipe-to-Delete: Kein Flackern mehr bei schnellem Löschen
- Scroll-to-Top nach Note Save (ListAdapter async fix)

📡 Sync-Architektur Dokumentation
- SYNC_ARCHITECTURE.md mit allen 4 Sync-Triggern
- DOCS.md + DOCS.en.md aktualisiert
- GitHub Actions: F-Droid Changelogs statt Commit-Messages

🎯 Testing: BUGFIX_SPURIOUS_SYNC_ERROR_NOTIFICATIONS.md
📦 Version: 1.1.1 (versionCode=3)
This commit is contained in:
inventory69
2025-12-26 12:18:51 +01:00
parent 7644f5bf76
commit 9b6bf04954
14 changed files with 381 additions and 32 deletions

View File

@@ -102,12 +102,29 @@ jobs:
echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "COMMIT_DATE=$(git log -1 --format=%cd --date=iso-strict)" >> $GITHUB_ENV echo "COMMIT_DATE=$(git log -1 --format=%cd --date=iso-strict)" >> $GITHUB_ENV
# Vollständige Commit-Nachricht mit Zeilenumbrüchen und Emojis (UTF-8) - name: F-Droid Changelogs lesen
run: |
# Lese deutsche Changelog (Hauptsprache)
if [ -f "android/fastlane/metadata/android/de-DE/changelogs/${{ env.BUILD_NUMBER }}.txt" ]; then
{ {
echo 'COMMIT_MSG<<EOF' echo 'CHANGELOG_DE<<EOF'
git -c core.quotepath=false log -1 --pretty=%B cat "android/fastlane/metadata/android/de-DE/changelogs/${{ env.BUILD_NUMBER }}.txt"
echo 'EOF' echo 'EOF'
} >> $GITHUB_ENV } >> $GITHUB_ENV
else
echo "CHANGELOG_DE=Keine deutschen Release Notes verfügbar." >> $GITHUB_ENV
fi
# Lese englische Changelog (optional)
if [ -f "android/fastlane/metadata/android/en-US/changelogs/${{ env.BUILD_NUMBER }}.txt" ]; then
{
echo 'CHANGELOG_EN<<EOF'
cat "android/fastlane/metadata/android/en-US/changelogs/${{ env.BUILD_NUMBER }}.txt"
echo 'EOF'
} >> $GITHUB_ENV
else
echo "CHANGELOG_EN=" >> $GITHUB_ENV
fi
- name: Create Production Release - name: Create Production Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
@@ -134,9 +151,16 @@ jobs:
--- ---
## 📋 Aenderungen ## 📋 Changelog / Release Notes
${{ env.COMMIT_MSG }} ${{ env.CHANGELOG_EN }}
<details>
<summary><3E>🇪 Deutsche Version (zum Aufklappen)</summary>
${{ env.CHANGELOG_DE }}
</details>
--- ---
@@ -148,6 +172,6 @@ jobs:
--- ---
**[<EFBFBD> Dokumentation](https://github.com/inventory69/simple-notes-sync/blob/main/QUICKSTART.md)** · **[🐛 Issue melden](https://github.com/inventory69/simple-notes-sync/issues)** **[📖 Dokumentation](https://github.com/inventory69/simple-notes-sync/blob/main/QUICKSTART.md)** · **[🐛 Issue melden](https://github.com/inventory69/simple-notes-sync/issues)**
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -118,7 +118,61 @@ fun isInHomeNetwork(): Boolean {
--- ---
## 🔋 Battery Optimization ## <EFBFBD> Sync Trigger Overview
The app uses **4 different sync triggers** with different use cases:
| Trigger | File | Function | When? | Pre-Check? |
|---------|------|----------|-------|------------|
| **1. Manual Sync** | `MainActivity.kt` | `triggerManualSync()` | User clicks sync button in menu | ✅ Yes |
| **2. Auto-Sync (onResume)** | `MainActivity.kt` | `triggerAutoSync()` | App opened/resumed | ✅ Yes |
| **3. Background Sync (Periodic)** | `SyncWorker.kt` | `doWork()` | Every 15/30/60 minutes (configurable) | ✅ Yes |
| **4. WiFi-Connect Sync** | `NetworkMonitor.kt``SyncWorker.kt` | `triggerWifiConnectSync()` | WiFi enabled/SSID changed | ✅ Yes |
### Server Reachability Check (Pre-Check)
**All 4 sync triggers** use a **pre-check** before the actual sync:
```kotlin
// 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
}
}
```
**Why Socket Check instead of HTTP Request?**
-**Faster:** Socket connect is instant, HTTP request takes longer
- 🔋 **Battery Efficient:** No HTTP overhead (headers, TLS handshake, etc.)
- 🎯 **More Precise:** Only checks network reachability, not server logic
- 🛡️ **Prevents Errors:** Detects foreign WiFi networks before sync error occurs
**When does the check fail?**
- ❌ Server offline/unreachable
- ❌ Wrong WiFi network (e.g. public café WiFi)
- ❌ Network not ready yet (DHCP/routing delay after WiFi connect)
- ❌ VPN blocks server access
- ❌ No WebDAV server URL configured
### Sync Behavior by Trigger Type
| Trigger | When server not reachable | On successful sync | Throttling |
|---------|--------------------------|-------------------|------------|
| Manual Sync | Toast: "Server not reachable" | Toast: "✅ Synced: X notes" | None |
| Auto-Sync (onResume) | Silent abort (no toast) | Toast: "✅ Synced: X notes" | Max. 1x/min |
| Background Sync | Silent abort (no toast) | Silent (LocalBroadcast only) | 15/30/60 min |
| WiFi-Connect Sync | Silent abort (no toast) | Silent (LocalBroadcast only) | SSID-based |
---
## <20>🔋 Battery Optimization
### Usage Analysis ### Usage Analysis
@@ -466,9 +520,10 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
## 📖 Further Documentation ## 📖 Further Documentation
- [Project Docs](https://github.com/inventory69/project-docs/tree/main/simple-notes-sync) - [Project Docs](https://github.com/inventory69/project-docs/tree/main/simple-notes-sync)
- [Sync Architecture](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/SYNC_ARCHITECTURE.md) - **Detailed Sync Trigger Documentation**
- [Android Guide](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/ANDROID_GUIDE.md) - [Android Guide](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/ANDROID_GUIDE.md)
- [Bugfix Documentation](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/BUGFIX_SYNC_SPAM_AND_NOTIFICATIONS.md) - [Bugfix Documentation](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/BUGFIX_SYNC_SPAM_AND_NOTIFICATIONS.md)
--- ---
**Last updated:** December 21, 2025 **Last updated:** December 25, 2025

59
DOCS.md
View File

@@ -118,7 +118,61 @@ fun isInHomeNetwork(): Boolean {
--- ---
## 🔋 Akku-Optimierung ## <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.kt``SyncWorker.kt` | `triggerWifiConnectSync()` | WiFi an/SSID-Wechsel | ✅ Ja |
### Server-Erreichbarkeits-Check (Pre-Check)
**Alle 4 Sync-Trigger** verwenden vor dem eigentlichen Sync einen **Pre-Check**:
```kotlin
// 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) | SSID-basiert |
---
## <20>🔋 Akku-Optimierung
### Verbrauchsanalyse ### Verbrauchsanalyse
@@ -466,9 +520,10 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
## 📖 Weitere Dokumentation ## 📖 Weitere Dokumentation
- [Project Docs](https://github.com/inventory69/project-docs/tree/main/simple-notes-sync) - [Project Docs](https://github.com/inventory69/project-docs/tree/main/simple-notes-sync)
- [Sync Architecture](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/SYNC_ARCHITECTURE.md) - **Detaillierte Sync-Trigger Dokumentation**
- [Android Guide](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/ANDROID_GUIDE.md) - [Android Guide](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/ANDROID_GUIDE.md)
- [Bugfix Documentation](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/BUGFIX_SYNC_SPAM_AND_NOTIFICATIONS.md) - [Bugfix Documentation](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/BUGFIX_SYNC_SPAM_AND_NOTIFICATIONS.md)
--- ---
**Letzte Aktualisierung:** 21. Dezember 2025 **Letzte Aktualisierung:** 25. Dezember 2025

View File

@@ -17,8 +17,8 @@ android {
applicationId = "dev.dettmer.simplenotes" applicationId = "dev.dettmer.simplenotes"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 2 // 🔥 F-Droid Release v1.1.0 versionCode = 3 // 🔥 Bugfix: Spurious Sync Error Notifications + Sync Icon Bug
versionName = "1.1.0" // 🔥 Configurable Sync Interval + About Section versionName = "1.1.1" // 🔥 Bugfix: Server-Erreichbarkeits-Check + Notification-Improvements
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -45,6 +45,9 @@ class MainActivity : AppCompatActivity() {
private lateinit var adapter: NotesAdapter private lateinit var adapter: NotesAdapter
private val storage by lazy { NotesStorage(this) } private val storage by lazy { NotesStorage(this) }
// Track pending deletions to prevent flicker when notes reload
private val pendingDeletions = mutableSetOf<String>()
private val prefs by lazy { private val prefs by lazy {
getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE) getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
} }
@@ -91,6 +94,9 @@ class MainActivity : AppCompatActivity() {
Logger.enableFileLogging(this) Logger.enableFileLogging(this)
} }
// Alte Sync-Notifications beim App-Start löschen
NotificationHelper.clearSyncNotifications(this)
// Permission für Notifications (Android 13+) // Permission für Notifications (Android 13+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestNotificationPermission() requestNotificationPermission()
@@ -117,7 +123,7 @@ class MainActivity : AppCompatActivity() {
Logger.d(TAG, "📡 BroadcastReceiver registered (sync-completed)") Logger.d(TAG, "📡 BroadcastReceiver registered (sync-completed)")
// Reload notes // Reload notes (scroll to top wird in loadNotes() gemacht)
loadNotes() loadNotes()
// Trigger Auto-Sync beim App-Wechsel in Vordergrund (Toast) // Trigger Auto-Sync beim App-Wechsel in Vordergrund (Toast)
@@ -142,10 +148,21 @@ class MainActivity : AppCompatActivity() {
// Update last sync timestamp // Update last sync timestamp
prefs.edit().putLong(PREF_LAST_AUTO_SYNC_TIME, System.currentTimeMillis()).apply() prefs.edit().putLong(PREF_LAST_AUTO_SYNC_TIME, System.currentTimeMillis()).apply()
// GLEICHER Sync-Code wie manueller Sync (funktioniert!)
lifecycleScope.launch { lifecycleScope.launch {
try { try {
val syncService = WebDavSyncService(this@MainActivity) val syncService = WebDavSyncService(this@MainActivity)
// ⭐ WICHTIG: Server-Erreichbarkeits-Check VOR Sync (wie in SyncWorker)
val isReachable = withContext(Dispatchers.IO) {
syncService.isServerReachable()
}
if (!isReachable) {
Logger.d(TAG, "⏭️ Auto-sync ($source): Server not reachable - skipping silently")
return@launch
}
// Server ist erreichbar → Sync durchführen
val result = withContext(Dispatchers.IO) { val result = withContext(Dispatchers.IO) {
syncService.syncNotes() syncService.syncNotes()
} }
@@ -236,6 +253,9 @@ class MainActivity : AppCompatActivity() {
val note = adapter.currentList[position] val note = adapter.currentList[position]
val notesCopy = adapter.currentList.toMutableList() val notesCopy = adapter.currentList.toMutableList()
// Track pending deletion to prevent flicker
pendingDeletions.add(note.id)
// Remove from list immediately for visual feedback // Remove from list immediately for visual feedback
notesCopy.removeAt(position) notesCopy.removeAt(position)
adapter.submitList(notesCopy) adapter.submitList(notesCopy)
@@ -246,13 +266,15 @@ class MainActivity : AppCompatActivity() {
"Notiz gelöscht", "Notiz gelöscht",
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
).setAction("RÜCKGÄNGIG") { ).setAction("RÜCKGÄNGIG") {
// UNDO: Restore note in list // UNDO: Remove from pending deletions and restore
pendingDeletions.remove(note.id)
loadNotes() loadNotes()
}.addCallback(object : Snackbar.Callback() { }.addCallback(object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
if (event != DISMISS_EVENT_ACTION) { if (event != DISMISS_EVENT_ACTION) {
// Snackbar dismissed without UNDO → Actually delete the note // Snackbar dismissed without UNDO → Actually delete the note
storage.deleteNote(note.id) storage.deleteNote(note.id)
pendingDeletions.remove(note.id)
loadNotes() loadNotes()
} }
} }
@@ -276,10 +298,21 @@ class MainActivity : AppCompatActivity() {
private fun loadNotes() { private fun loadNotes() {
val notes = storage.loadAllNotes() val notes = storage.loadAllNotes()
adapter.submitList(notes)
// Filter out notes that are pending deletion (prevent flicker)
val filteredNotes = notes.filter { it.id !in pendingDeletions }
// Submit list with callback to scroll to top after list is updated
adapter.submitList(filteredNotes) {
// Scroll to top after list update is complete
// Wichtig: Nach dem Erstellen/Bearbeiten einer Notiz
if (filteredNotes.isNotEmpty()) {
recyclerViewNotes.scrollToPosition(0)
}
}
// Material 3 Empty State Card // Material 3 Empty State Card
emptyStateCard.visibility = if (notes.isEmpty()) { emptyStateCard.visibility = if (filteredNotes.isEmpty()) {
android.view.View.VISIBLE android.view.View.VISIBLE
} else { } else {
android.view.View.GONE android.view.View.GONE
@@ -305,8 +338,21 @@ class MainActivity : AppCompatActivity() {
try { try {
showToast("Starte Synchronisation...") showToast("Starte Synchronisation...")
// Start sync // Create sync service
val syncService = WebDavSyncService(this@MainActivity) val syncService = WebDavSyncService(this@MainActivity)
// ⭐ WICHTIG: Server-Erreichbarkeits-Check VOR Sync (wie in SyncWorker)
val isReachable = withContext(Dispatchers.IO) {
syncService.isServerReachable()
}
if (!isReachable) {
Logger.d(TAG, "⏭️ Manual Sync: Server not reachable - aborting")
showToast("Server nicht erreichbar")
return@launch
}
// Server ist erreichbar → Sync durchführen
val result = withContext(Dispatchers.IO) { val result = withContext(Dispatchers.IO) {
syncService.syncNotes() syncService.syncNotes()
} }

View File

@@ -1,5 +1,6 @@
package dev.dettmer.simplenotes.adapters package dev.dettmer.simplenotes.adapters
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -11,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView
import dev.dettmer.simplenotes.R import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.Note import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.models.SyncStatus import dev.dettmer.simplenotes.models.SyncStatus
import dev.dettmer.simplenotes.utils.Constants
import dev.dettmer.simplenotes.utils.toReadableTime import dev.dettmer.simplenotes.utils.toReadableTime
import dev.dettmer.simplenotes.utils.truncate import dev.dettmer.simplenotes.utils.truncate
@@ -39,6 +41,12 @@ class NotesAdapter(
textViewContent.text = note.content.truncate(100) textViewContent.text = note.content.truncate(100)
textViewTimestamp.text = note.updatedAt.toReadableTime() textViewTimestamp.text = note.updatedAt.toReadableTime()
// Sync Icon nur zeigen wenn Sync konfiguriert ist
val prefs = itemView.context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
val isSyncConfigured = !serverUrl.isNullOrEmpty()
if (isSyncConfigured) {
// Sync status icon // Sync status icon
val syncIcon = when (note.syncStatus) { val syncIcon = when (note.syncStatus) {
SyncStatus.SYNCED -> android.R.drawable.ic_menu_upload SyncStatus.SYNCED -> android.R.drawable.ic_menu_upload
@@ -47,6 +55,11 @@ class NotesAdapter(
SyncStatus.LOCAL_ONLY -> android.R.drawable.ic_menu_save SyncStatus.LOCAL_ONLY -> android.R.drawable.ic_menu_save
} }
imageViewSyncStatus.setImageResource(syncIcon) imageViewSyncStatus.setImageResource(syncIcon)
imageViewSyncStatus.visibility = View.VISIBLE
} else {
// Sync nicht konfiguriert → Icon verstecken
imageViewSyncStatus.visibility = View.GONE
}
itemView.setOnClickListener { itemView.setOnClickListener {
onNoteClick(note) onNoteClick(note)

View File

@@ -52,7 +52,28 @@ class SyncWorker(
} }
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 2: Before syncNotes() call") Logger.d(TAG, "📍 Step 2: Checking server reachability (Pre-Check)")
}
// ⭐ KRITISCH: Server-Erreichbarkeits-Check VOR Sync
// Verhindert Fehler-Notifications in fremden WiFi-Netzen
// Wartet bis Netzwerk bereit ist (DHCP, Routing, Gateway)
if (!syncService.isServerReachable()) {
Logger.d(TAG, "⏭️ Server not reachable - skipping sync (no error)")
Logger.d(TAG, " Reason: Server offline/wrong network/network not ready/not configured")
Logger.d(TAG, " This is normal in foreign WiFi or during network initialization")
if (BuildConfig.DEBUG) {
Logger.d(TAG, "✅ SyncWorker.doWork() SUCCESS (silent skip)")
Logger.d(TAG, "═══════════════════════════════════════")
}
// Success zurückgeben (kein Fehler, Server ist halt nicht erreichbar)
return@withContext Result.success()
}
if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 3: Server reachable - proceeding with sync")
Logger.d(TAG, " SyncService: $syncService") Logger.d(TAG, " SyncService: $syncService")
} }
@@ -73,13 +94,13 @@ class SyncWorker(
} }
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 3: Processing result") Logger.d(TAG, "📍 Step 4: Processing result")
Logger.d(TAG, "📦 Sync result: success=${result.isSuccess}, count=${result.syncedCount}, error=${result.errorMessage}") Logger.d(TAG, "📦 Sync result: success=${result.isSuccess}, count=${result.syncedCount}, error=${result.errorMessage}")
} }
if (result.isSuccess) { if (result.isSuccess) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 4: Success path") Logger.d(TAG, "📍 Step 5: Success path")
} }
Logger.i(TAG, "✅ Sync successful: ${result.syncedCount} notes") Logger.i(TAG, "✅ Sync successful: ${result.syncedCount} notes")
@@ -109,7 +130,7 @@ class SyncWorker(
Result.success() Result.success()
} else { } else {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 4: Failure path") Logger.d(TAG, "📍 Step 5: Failure path")
} }
Logger.e(TAG, "❌ Sync failed: ${result.errorMessage}") Logger.e(TAG, "❌ Sync failed: ${result.errorMessage}")
NotificationHelper.showSyncError( NotificationHelper.showSyncError(

View File

@@ -20,6 +20,7 @@ import java.net.InetSocketAddress
import java.net.NetworkInterface import java.net.NetworkInterface
import java.net.Proxy import java.net.Proxy
import java.net.Socket import java.net.Socket
import java.net.URL
import javax.net.SocketFactory import javax.net.SocketFactory
class WebDavSyncService(private val context: Context) { class WebDavSyncService(private val context: Context) {
@@ -188,6 +189,40 @@ class WebDavSyncService(private val context: Context) {
return prefs.getString(Constants.KEY_SERVER_URL, null) return prefs.getString(Constants.KEY_SERVER_URL, null)
} }
/**
* Prüft ob WebDAV-Server erreichbar ist (ohne Sync zu starten)
* Verwendet Socket-Check für schnelle Erreichbarkeitsprüfung
*
* @return true wenn Server erreichbar ist, false sonst
*/
suspend fun isServerReachable(): Boolean = withContext(Dispatchers.IO) {
return@withContext try {
val serverUrl = getServerUrl()
if (serverUrl == null) {
Logger.d(TAG, "❌ Server URL not configured")
return@withContext false
}
val url = URL(serverUrl)
val host = url.host
val port = if (url.port > 0) url.port else url.defaultPort
Logger.d(TAG, "🔍 Checking server reachability: $host:$port")
// Socket-Check mit 2s Timeout
// Gibt dem Netzwerk Zeit für Initialisierung (DHCP, Routing, Gateway)
val socket = Socket()
socket.connect(InetSocketAddress(host, port), 2000)
socket.close()
Logger.d(TAG, "✅ Server is reachable")
true
} catch (e: Exception) {
Logger.d(TAG, "❌ Server not reachable: ${e.message}")
false
}
}
suspend fun testConnection(): SyncResult = withContext(Dispatchers.IO) { suspend fun testConnection(): SyncResult = withContext(Dispatchers.IO) {
return@withContext try { return@withContext try {
val sardine = getSardine() ?: return@withContext SyncResult( val sardine = getSardine() ?: return@withContext SyncResult(

View File

@@ -6,12 +6,15 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import dev.dettmer.simplenotes.MainActivity import dev.dettmer.simplenotes.MainActivity
object NotificationHelper { object NotificationHelper {
private const val TAG = "NotificationHelper"
private const val CHANNEL_ID = "notes_sync_channel" private const val CHANNEL_ID = "notes_sync_channel"
private const val CHANNEL_NAME = "Notizen Synchronisierung" private const val CHANNEL_NAME = "Notizen Synchronisierung"
private const val CHANNEL_DESCRIPTION = "Benachrichtigungen über Sync-Status" private const val CHANNEL_DESCRIPTION = "Benachrichtigungen über Sync-Status"
@@ -38,6 +41,17 @@ object NotificationHelper {
} }
} }
/**
* Löscht alle Sync-Notifications
* Sollte beim App-Start aufgerufen werden um alte Notifications zu entfernen
*/
fun clearSyncNotifications(context: Context) {
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager
manager.cancel(SYNC_NOTIFICATION_ID)
Logger.d(TAG, "🗑️ Cleared old sync notifications")
}
/** /**
* Zeigt Erfolgs-Notification nach Sync * Zeigt Erfolgs-Notification nach Sync
*/ */
@@ -240,6 +254,7 @@ object NotificationHelper {
/** /**
* Zeigt Fehler-Notification * Zeigt Fehler-Notification
* Auto-Cancel nach 30 Sekunden
*/ */
fun showSyncError(context: Context, message: String) { fun showSyncError(context: Context, message: String) {
// PendingIntent für App-Öffnung // PendingIntent für App-Öffnung
@@ -266,5 +281,11 @@ object NotificationHelper {
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager as NotificationManager
manager.notify(SYNC_NOTIFICATION_ID, notification) manager.notify(SYNC_NOTIFICATION_ID, notification)
// ⭐ NEU: Auto-Cancel nach 30 Sekunden
Handler(Looper.getMainLooper()).postDelayed({
manager.cancel(SYNC_NOTIFICATION_ID)
Logger.d(TAG, "🗑️ Auto-cancelled error notification after 30s timeout")
}, 30_000)
} }
} }

View File

@@ -0,0 +1,20 @@
🐛 Bugfixes v1.1.1
✅ Keine Fehler-Notifications mehr in fremden WiFi-Netzwerken!
- Server-Erreichbarkeits-Check vor jedem Sync (2s Timeout)
- Stiller Abbruch wenn Server nicht erreichbar
- 80% schnellerer Abbruch: 2s statt 10+ Sekunden
✅ Keine Fehler beim WiFi-Connect / Nach-Hause-Kommen!
- Pre-Check wartet bis Netzwerk bereit ist (DHCP, Routing, Gateway)
- Kein Fehler mehr bei Netzwerk-Initialisierung
🔧 Notification-Verbesserungen:
- Alte Notifications werden beim App-Start gelöscht
- Fehler-Notifications verschwinden automatisch nach 30 Sekunden
- Bessere Batterie-Effizienz (keine langen Timeouts mehr)
📱 UI-Fixes:
- Sync-Icon wird nicht mehr angezeigt wenn Sync nicht konfiguriert ist
- Swipe-to-Delete: Kein Flackern mehr beim schnellen Löschen mehrerer Notizen
- Nach dem Speichern einer Notiz landet man automatisch ganz oben in der Liste

View File

@@ -0,0 +1,20 @@
🐛 Bugfixes v1.1.1
✅ No more error notifications in foreign WiFi networks!
- Server reachability check before each sync (2s timeout)
- Silent abort when server is unreachable
- 80% faster abort: 2s instead of 10+ seconds
✅ No more errors when connecting to WiFi / arriving home!
- Pre-check waits until network is ready (DHCP, routing, gateway)
- No more errors during network initialization
🔧 Notification improvements:
- Old notifications are cleared on app start
- Error notifications disappear automatically after 30 seconds
- Better battery efficiency (no more long timeouts)
📱 UI fixes:
- Sync icon no longer shown when sync is not configured
- Swipe-to-delete: No more flickering when quickly deleting multiple notes
- After saving a note, you automatically land at the top of the list

View File

@@ -0,0 +1,37 @@
Simple Notes Sync is a minimalist note-taking app with WebDAV synchronization.
KEY FEATURES:
• Create and edit simple notes
• WebDAV synchronization with your own server
• Automatic synchronization on home WiFi
• Configurable sync interval (15/30/60 minutes)
• Transparent battery usage display
• Material Design 3 with Dynamic Colors (Android 12+)
• Swipe-to-delete with confirmation dialog
• Server backup & restore
• Fully usable offline
• No ads, no trackers
PRIVACY:
Your data stays with you! The app only communicates with your own WebDAV server. No cloud services, no tracking libraries, no analytics tools.
SYNCHRONIZATION:
• Supports all WebDAV servers (Nextcloud, ownCloud, etc.)
• Configurable interval: 15, 30, or 60 minutes
• Measured battery consumption: only ~0.4% per day (at 30min)
• Doze Mode optimized for reliable background syncs
• Manual synchronization available anytime
• Conflict-free merging through timestamps
MATERIAL DESIGN 3:
• Modern user interface
• Dynamic Colors (Material You) on Android 12+
• Dark Mode support
• Intuitive gestures (Swipe-to-delete)
Open Source under MIT License
Source code: https://github.com/inventory69/simple-notes-sync

View File

@@ -0,0 +1 @@
Simple note-taking app with WebDAV synchronization

View File

@@ -0,0 +1 @@
Simple Notes Sync