🐛 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:
36
.github/workflows/build-production-apk.yml
vendored
36
.github/workflows/build-production-apk.yml
vendored
@@ -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 }}
|
||||||
|
|||||||
59
DOCS.en.md
59
DOCS.en.md
@@ -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
59
DOCS.md
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
android/fastlane/metadata/android/de-DE/changelogs/3.txt
Normal file
20
android/fastlane/metadata/android/de-DE/changelogs/3.txt
Normal 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
|
||||||
20
android/fastlane/metadata/android/en-US/changelogs/3.txt
Normal file
20
android/fastlane/metadata/android/en-US/changelogs/3.txt
Normal 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
|
||||||
37
android/fastlane/metadata/android/en-US/full_description.txt
Normal file
37
android/fastlane/metadata/android/en-US/full_description.txt
Normal 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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Simple note-taking app with WebDAV synchronization
|
||||||
1
android/fastlane/metadata/android/en-US/title.txt
Normal file
1
android/fastlane/metadata/android/en-US/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Simple Notes Sync
|
||||||
Reference in New Issue
Block a user