🐛 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 "COMMIT_DATE=$(git log -1 --format=%cd --date=iso-strict)" >> $GITHUB_ENV
# Vollständige Commit-Nachricht mit Zeilenumbrüchen und Emojis (UTF-8)
{
echo 'COMMIT_MSG<<EOF'
git -c core.quotepath=false log -1 --pretty=%B
echo 'EOF'
} >> $GITHUB_ENV
- 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 'CHANGELOG_DE<<EOF'
cat "android/fastlane/metadata/android/de-DE/changelogs/${{ env.BUILD_NUMBER }}.txt"
echo 'EOF'
} >> $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
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:
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
@@ -466,9 +520,10 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
## 📖 Further Documentation
- [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)
- [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
@@ -466,9 +520,10 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
## 📖 Weitere Dokumentation
- [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)
- [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"
minSdk = 24
targetSdk = 36
versionCode = 2 // 🔥 F-Droid Release v1.1.0
versionName = "1.1.0" // 🔥 Configurable Sync Interval + About Section
versionCode = 3 // 🔥 Bugfix: Spurious Sync Error Notifications + Sync Icon Bug
versionName = "1.1.1" // 🔥 Bugfix: Server-Erreichbarkeits-Check + Notification-Improvements
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -45,6 +45,9 @@ class MainActivity : AppCompatActivity() {
private lateinit var adapter: NotesAdapter
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 {
getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
}
@@ -91,6 +94,9 @@ class MainActivity : AppCompatActivity() {
Logger.enableFileLogging(this)
}
// Alte Sync-Notifications beim App-Start löschen
NotificationHelper.clearSyncNotifications(this)
// Permission für Notifications (Android 13+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestNotificationPermission()
@@ -117,7 +123,7 @@ class MainActivity : AppCompatActivity() {
Logger.d(TAG, "📡 BroadcastReceiver registered (sync-completed)")
// Reload notes
// Reload notes (scroll to top wird in loadNotes() gemacht)
loadNotes()
// Trigger Auto-Sync beim App-Wechsel in Vordergrund (Toast)
@@ -142,10 +148,21 @@ class MainActivity : AppCompatActivity() {
// Update last sync timestamp
prefs.edit().putLong(PREF_LAST_AUTO_SYNC_TIME, System.currentTimeMillis()).apply()
// GLEICHER Sync-Code wie manueller Sync (funktioniert!)
lifecycleScope.launch {
try {
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) {
syncService.syncNotes()
}
@@ -236,6 +253,9 @@ class MainActivity : AppCompatActivity() {
val note = adapter.currentList[position]
val notesCopy = adapter.currentList.toMutableList()
// Track pending deletion to prevent flicker
pendingDeletions.add(note.id)
// Remove from list immediately for visual feedback
notesCopy.removeAt(position)
adapter.submitList(notesCopy)
@@ -246,13 +266,15 @@ class MainActivity : AppCompatActivity() {
"Notiz gelöscht",
Snackbar.LENGTH_LONG
).setAction("RÜCKGÄNGIG") {
// UNDO: Restore note in list
// UNDO: Remove from pending deletions and restore
pendingDeletions.remove(note.id)
loadNotes()
}.addCallback(object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
if (event != DISMISS_EVENT_ACTION) {
// Snackbar dismissed without UNDO → Actually delete the note
storage.deleteNote(note.id)
pendingDeletions.remove(note.id)
loadNotes()
}
}
@@ -276,10 +298,21 @@ class MainActivity : AppCompatActivity() {
private fun loadNotes() {
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
emptyStateCard.visibility = if (notes.isEmpty()) {
emptyStateCard.visibility = if (filteredNotes.isEmpty()) {
android.view.View.VISIBLE
} else {
android.view.View.GONE
@@ -305,8 +338,21 @@ class MainActivity : AppCompatActivity() {
try {
showToast("Starte Synchronisation...")
// Start sync
// Create sync service
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) {
syncService.syncNotes()
}

View File

@@ -1,5 +1,6 @@
package dev.dettmer.simplenotes.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -11,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.models.SyncStatus
import dev.dettmer.simplenotes.utils.Constants
import dev.dettmer.simplenotes.utils.toReadableTime
import dev.dettmer.simplenotes.utils.truncate
@@ -39,14 +41,25 @@ class NotesAdapter(
textViewContent.text = note.content.truncate(100)
textViewTimestamp.text = note.updatedAt.toReadableTime()
// Sync status icon
val syncIcon = when (note.syncStatus) {
SyncStatus.SYNCED -> android.R.drawable.ic_menu_upload
SyncStatus.PENDING -> android.R.drawable.ic_popup_sync
SyncStatus.CONFLICT -> android.R.drawable.ic_dialog_alert
SyncStatus.LOCAL_ONLY -> android.R.drawable.ic_menu_save
// 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
val syncIcon = when (note.syncStatus) {
SyncStatus.SYNCED -> android.R.drawable.ic_menu_upload
SyncStatus.PENDING -> android.R.drawable.ic_popup_sync
SyncStatus.CONFLICT -> android.R.drawable.ic_dialog_alert
SyncStatus.LOCAL_ONLY -> android.R.drawable.ic_menu_save
}
imageViewSyncStatus.setImageResource(syncIcon)
imageViewSyncStatus.visibility = View.VISIBLE
} else {
// Sync nicht konfiguriert → Icon verstecken
imageViewSyncStatus.visibility = View.GONE
}
imageViewSyncStatus.setImageResource(syncIcon)
itemView.setOnClickListener {
onNoteClick(note)

View File

@@ -52,7 +52,28 @@ class SyncWorker(
}
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")
}
@@ -73,13 +94,13 @@ class SyncWorker(
}
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}")
}
if (result.isSuccess) {
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")
@@ -109,7 +130,7 @@ class SyncWorker(
Result.success()
} else {
if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 4: Failure path")
Logger.d(TAG, "📍 Step 5: Failure path")
}
Logger.e(TAG, "❌ Sync failed: ${result.errorMessage}")
NotificationHelper.showSyncError(

View File

@@ -20,6 +20,7 @@ import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.net.Proxy
import java.net.Socket
import java.net.URL
import javax.net.SocketFactory
class WebDavSyncService(private val context: Context) {
@@ -188,6 +189,40 @@ class WebDavSyncService(private val context: Context) {
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) {
return@withContext try {
val sardine = getSardine() ?: return@withContext SyncResult(

View File

@@ -6,12 +6,15 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import dev.dettmer.simplenotes.MainActivity
object NotificationHelper {
private const val TAG = "NotificationHelper"
private const val CHANNEL_ID = "notes_sync_channel"
private const val CHANNEL_NAME = "Notizen Synchronisierung"
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
*/
@@ -240,6 +254,7 @@ object NotificationHelper {
/**
* Zeigt Fehler-Notification
* Auto-Cancel nach 30 Sekunden
*/
fun showSyncError(context: Context, message: String) {
// PendingIntent für App-Öffnung
@@ -266,5 +281,11 @@ object NotificationHelper {
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager
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