🐛 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:
42
.github/workflows/build-production-apk.yml
vendored
42
.github/workflows/build-production-apk.yml
vendored
@@ -101,13 +101,30 @@ jobs:
|
||||
run: |
|
||||
echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
echo "COMMIT_DATE=$(git log -1 --format=%cd --date=iso-strict)" >> $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
|
||||
|
||||
# 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
|
||||
# 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 }}
|
||||
|
||||
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
|
||||
|
||||
@@ -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
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
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