Skip to content

Commit df4ee4b

Browse files
committed
v1.7.1: Fix Android 9 crash and Kernel-VPN compatibility
- Fix connection leak on Android 9 (close() in finally block) - Fix VPN detection for Kernel Wireguard (interface name patterns) - Fix missing files after app data clear (local existence check) - Update changelogs for v1.7.1 (versionCode 18) Refs: #15
1 parent 68e8490 commit df4ee4b

9 files changed

Lines changed: 166 additions & 49 deletions

File tree

CHANGELOG.de.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,33 @@ Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
88

99
---
1010

11+
## [1.7.1] - 2026-01-30
12+
13+
### 🐛 Kritische Fehlerbehebungen
14+
15+
- **App-Absturz auf Android 9 nach längerer Nutzung behoben** ([ref #15](https://github.com/inventory69/simple-notes-sync/issues/15))
16+
- Ressourcenerschöpfung durch nicht geschlossene HTTP-Verbindungen behoben
17+
- App konnte nach ~30-45 Minuten Nutzung durch angesammelte Connection-Leaks abstürzen
18+
- Danke an [@roughnecks] für den detaillierten Fehlerbericht!
19+
20+
- **VPN-Kompatibilitäts-Regression behoben** ([ref #11](https://github.com/inventory69/simple-notes-sync/issues/11))
21+
- WiFi Socket-Binding erkennt jetzt korrekt Wireguard VPN-Interfaces (tun*, wg*, *-wg-*)
22+
- Traffic wird korrekt durch VPN-Tunnel geleitet statt direkt über WiFi
23+
- Behebt "Verbindungs-Timeout" beim Sync zu externen Servern über VPN
24+
25+
### 🔧 Technische Änderungen
26+
27+
- Neue `SafeSardineWrapper` Klasse stellt korrektes HTTP-Connection-Cleanup sicher
28+
- Weniger unnötige 401-Authentifizierungs-Challenges durch preemptive Auth-Header
29+
- ProGuard-Regel hinzugefügt um harmlose TextInclusionStrategy-Warnungen zu unterdrücken
30+
- VPN-Interface-Erkennung via `NetworkInterface.getNetworkInterfaces()` Pattern-Matching
31+
32+
### 🌍 Lokalisierung
33+
34+
- Hardcodierte deutsche Fehlermeldungen behoben - jetzt String-Resources für korrekte Lokalisierung
35+
36+
---
37+
1138
## [1.7.0] - 2026-01-26
1239

1340
### 🎉 Major: Grid-Ansicht, Nur-WLAN Sync & VPN-Unterstützung

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1717
- App could crash after ~30-45 minutes of use due to accumulated connection leaks
1818
- Thanks to [@roughnecks] for the detailed bug report!
1919

20+
- **Fixed VPN compatibility regression** ([ref #11](https://github.com/inventory69/simple-notes-sync/issues/11))
21+
- WiFi socket binding now correctly detects Wireguard VPN interfaces (tun*, wg*, *-wg-*)
22+
- Traffic routes through VPN tunnel instead of bypassing it directly to WiFi
23+
- Fixes "Connection timeout" when syncing to external servers via VPN
24+
2025
### 🔧 Technical Changes
2126

2227
- New `SafeSardineWrapper` class ensures proper HTTP connection cleanup
2328
- Reduced unnecessary 401 authentication challenges with preemptive auth headers
2429
- Added ProGuard rule to suppress harmless TextInclusionStrategy warnings on older Android versions
30+
- VPN interface detection via `NetworkInterface.getNetworkInterfaces()` pattern matching
31+
32+
### 🌍 Localization
33+
34+
- Fixed hardcoded German error messages - now uses string resources for proper localization
2535

2636
---
2737

android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -392,21 +392,21 @@ class MainActivity : AppCompatActivity() {
392392
// 🔥 v1.1.2: Check if there are unsynced changes first (performance optimization)
393393
if (!syncService.hasUnsyncedChanges()) {
394394
Logger.d(TAG, "⏭️ No unsynced changes, skipping server reachability check")
395-
SyncStateManager.markCompleted("Bereits synchronisiert")
395+
SyncStateManager.markCompleted(getString(R.string.snackbar_already_synced))
396396
return@launch
397397
}
398398

399399
// Check if server is reachable
400400
if (!syncService.isServerReachable()) {
401-
SyncStateManager.markError("Server nicht erreichbar")
401+
SyncStateManager.markError(getString(R.string.snackbar_server_unreachable))
402402
return@launch
403403
}
404404

405405
// Perform sync
406406
val result = syncService.syncNotes()
407407

408408
if (result.isSuccess) {
409-
SyncStateManager.markCompleted("${result.syncedCount} Notizen")
409+
SyncStateManager.markCompleted(getString(R.string.snackbar_synced_count, result.syncedCount))
410410
loadNotes()
411411
} else {
412412
SyncStateManager.markError(result.errorMessage)
@@ -683,7 +683,7 @@ class MainActivity : AppCompatActivity() {
683683

684684
if (!isReachable) {
685685
Logger.d(TAG, "⏭️ Manual Sync: Server not reachable - aborting")
686-
SyncStateManager.markError("Server nicht erreichbar")
686+
SyncStateManager.markError(getString(R.string.snackbar_server_unreachable))
687687
return@launch
688688
}
689689

android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -608,8 +608,8 @@ class SettingsActivity : AppCompatActivity() {
608608

609609
// ⭐ WICHTIG: Server-Erreichbarkeits-Check VOR Sync (wie in anderen Triggern)
610610
if (!syncService.isServerReachable()) {
611-
showToast("⚠️ Server nicht erreichbar")
612-
SyncStateManager.markError("Server nicht erreichbar")
611+
showToast("⚠️ ${getString(R.string.snackbar_server_unreachable)}")
612+
SyncStateManager.markError(getString(R.string.snackbar_server_unreachable))
613613
checkServerStatus() // Server-Status aktualisieren
614614
return@launch
615615
}

android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt

Lines changed: 97 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,45 @@ class WebDavSyncService(private val context: Context) {
104104
return sessionWifiAddress
105105
}
106106

107+
/**
108+
* 🔒 v1.7.1: Checks if any VPN/Wireguard interface is active.
109+
*
110+
* Wireguard VPNs run as separate network interfaces (tun*, wg*, *-wg-*),
111+
* and are NOT detected via NetworkCapabilities.TRANSPORT_VPN!
112+
*
113+
* @return true if VPN interface is detected
114+
*/
115+
private fun isVpnInterfaceActive(): Boolean {
116+
try {
117+
val interfaces = NetworkInterface.getNetworkInterfaces() ?: return false
118+
while (interfaces.hasMoreElements()) {
119+
val iface = interfaces.nextElement()
120+
if (!iface.isUp) continue
121+
122+
val name = iface.name.lowercase()
123+
// Check for VPN/Wireguard interface patterns:
124+
// - tun0, tun1, etc. (OpenVPN, generic VPN)
125+
// - wg0, wg1, etc. (Wireguard)
126+
// - *-wg-* (Mullvad, ProtonVPN style: se-sto-wg-202)
127+
if (name.startsWith("tun") ||
128+
name.startsWith("wg") ||
129+
name.contains("-wg-") ||
130+
name.startsWith("ppp")) {
131+
Logger.d(TAG, "🔒 VPN interface detected: ${iface.name}")
132+
return true
133+
}
134+
}
135+
} catch (e: Exception) {
136+
Logger.w(TAG, "⚠️ Failed to check VPN interfaces: ${e.message}")
137+
}
138+
return false
139+
}
140+
107141
/**
108142
* Findet WiFi Interface IP-Adresse (um VPN zu umgehen)
143+
*
144+
* 🔒 v1.7.1 Fix: Now detects Wireguard VPN interfaces and skips WiFi binding
145+
* when VPN is active, so traffic routes through VPN tunnel correctly.
109146
*/
110147
@Suppress("ReturnCount") // Early returns for network validation checks
111148
private fun getWiFiInetAddressInternal(): InetAddress? {
@@ -129,10 +166,17 @@ class WebDavSyncService(private val context: Context) {
129166
return null
130167
}
131168

132-
// 🔒 v1.7.0: VPN-Detection - Skip WiFi binding when VPN is active
169+
// 🔒 v1.7.0: VPN-Detection via NetworkCapabilities (standard Android VPN)
133170
// When VPN is active, traffic should route through VPN, not directly via WiFi
134171
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
135-
Logger.d(TAG, "🔒 VPN detected - using default routing (traffic will go through VPN)")
172+
Logger.d(TAG, "🔒 VPN detected (TRANSPORT_VPN) - using default routing")
173+
return null
174+
}
175+
176+
// 🔒 v1.7.1: VPN-Detection via interface names (Wireguard, OpenVPN, etc.)
177+
// Wireguard VPNs are NOT detected via TRANSPORT_VPN, they run as separate interfaces!
178+
if (isVpnInterfaceActive()) {
179+
Logger.d(TAG, "🔒 VPN interface detected - skip WiFi binding, use default routing")
136180
return null
137181
}
138182

@@ -142,7 +186,7 @@ class WebDavSyncService(private val context: Context) {
142186
return null
143187
}
144188

145-
Logger.d(TAG, "✅ Network is WiFi, searching for interface...")
189+
Logger.d(TAG, "✅ Network is WiFi (no VPN), searching for interface...")
146190

147191
@Suppress("LoopWithTooManyJumpStatements") // Network interface filtering requires multiple conditions
148192
// Finde WiFi Interface
@@ -649,19 +693,19 @@ class WebDavSyncService(private val context: Context) {
649693
SyncResult(
650694
isSuccess = false,
651695
errorMessage = when (e) {
652-
is java.net.UnknownHostException -> "Server nicht erreichbar"
653-
is java.net.SocketTimeoutException -> "Verbindungs-Timeout"
654-
is javax.net.ssl.SSLException -> "SSL-Fehler"
696+
is java.net.UnknownHostException -> context.getString(R.string.snackbar_server_unreachable)
697+
is java.net.SocketTimeoutException -> context.getString(R.string.snackbar_connection_timeout)
698+
is javax.net.ssl.SSLException -> context.getString(R.string.sync_error_ssl)
655699
is com.thegrizzlylabs.sardineandroid.impl.SardineException -> {
656700
when (e.statusCode) {
657-
401 -> "Authentifizierung fehlgeschlagen"
658-
403 -> "Zugriff verweigert"
659-
404 -> "Server-Pfad nicht gefunden"
660-
500 -> "Server-Fehler"
661-
else -> "HTTP-Fehler: ${e.statusCode}"
701+
401 -> context.getString(R.string.sync_error_auth_failed)
702+
403 -> context.getString(R.string.sync_error_access_denied)
703+
404 -> context.getString(R.string.sync_error_path_not_found)
704+
500 -> context.getString(R.string.sync_error_server)
705+
else -> context.getString(R.string.sync_error_http, e.statusCode)
662706
}
663707
}
664-
else -> e.message ?: "Unbekannter Fehler"
708+
else -> e.message ?: context.getString(R.string.sync_error_unknown)
665709
}
666710
)
667711
}
@@ -824,19 +868,19 @@ class WebDavSyncService(private val context: Context) {
824868
SyncResult(
825869
isSuccess = false,
826870
errorMessage = when (e) {
827-
is java.net.UnknownHostException -> "Server nicht erreichbar: ${e.message}"
828-
is java.net.SocketTimeoutException -> "Verbindungs-Timeout: ${e.message}"
829-
is javax.net.ssl.SSLException -> "SSL-Fehler"
871+
is java.net.UnknownHostException -> "${context.getString(R.string.snackbar_server_unreachable)}: ${e.message}"
872+
is java.net.SocketTimeoutException -> "${context.getString(R.string.snackbar_connection_timeout)}: ${e.message}"
873+
is javax.net.ssl.SSLException -> context.getString(R.string.sync_error_ssl)
830874
is com.thegrizzlylabs.sardineandroid.impl.SardineException -> {
831875
when (e.statusCode) {
832-
401 -> "Authentifizierung fehlgeschlagen"
833-
403 -> "Zugriff verweigert"
834-
404 -> "Server-Pfad nicht gefunden"
835-
500 -> "Server-Fehler"
836-
else -> "HTTP-Fehler: ${e.statusCode}"
876+
401 -> context.getString(R.string.sync_error_auth_failed)
877+
403 -> context.getString(R.string.sync_error_access_denied)
878+
404 -> context.getString(R.string.sync_error_path_not_found)
879+
500 -> context.getString(R.string.sync_error_server)
880+
else -> context.getString(R.string.sync_error_http, e.statusCode)
837881
}
838882
}
839-
else -> e.message ?: "Unbekannter Fehler"
883+
else -> e.message ?: context.getString(R.string.sync_error_unknown)
840884
}
841885
)
842886
}
@@ -1145,9 +1189,32 @@ class WebDavSyncService(private val context: Context) {
11451189
"modified=$serverModified lastSync=$lastSyncTime"
11461190
)
11471191

1192+
// FIRST: Check deletion tracker - if locally deleted, skip unless re-created on server
1193+
if (deletionTracker.isDeleted(noteId)) {
1194+
val deletedAt = deletionTracker.getDeletionTimestamp(noteId)
1195+
1196+
// Smart check: Was note re-created on server after deletion?
1197+
if (deletedAt != null && serverModified > deletedAt) {
1198+
Logger.d(TAG, " 📝 Note re-created on server after deletion: $noteId")
1199+
deletionTracker.removeDeletion(noteId)
1200+
trackerModified = true
1201+
// Continue with download below
1202+
} else {
1203+
Logger.d(TAG, " ⏭️ Skipping deleted note: $noteId")
1204+
skippedDeleted++
1205+
processedIds.add(noteId)
1206+
continue
1207+
}
1208+
}
1209+
1210+
// Check if file exists locally
1211+
val localNote = storage.loadNote(noteId)
1212+
val fileExistsLocally = localNote != null
1213+
11481214
// PRIMARY: Timestamp check (works on first sync!)
11491215
// Same logic as Markdown sync - skip if not modified since last sync
1150-
if (!forceOverwrite && lastSyncTime > 0 && serverModified <= lastSyncTime) {
1216+
// BUT: Always download if file doesn't exist locally!
1217+
if (!forceOverwrite && fileExistsLocally && lastSyncTime > 0 && serverModified <= lastSyncTime) {
11511218
skippedUnchanged++
11521219
Logger.d(TAG, " ⏭️ Skipping $noteId: Not modified since last sync (timestamp)")
11531220
processedIds.add(noteId)
@@ -1156,13 +1223,19 @@ class WebDavSyncService(private val context: Context) {
11561223

11571224
// SECONDARY: E-Tag check (for performance after first sync)
11581225
// Catches cases where file was re-uploaded with same content
1159-
if (!forceOverwrite && serverETag != null && serverETag == cachedETag) {
1226+
// BUT: Always download if file doesn't exist locally!
1227+
if (!forceOverwrite && fileExistsLocally && serverETag != null && serverETag == cachedETag) {
11601228
skippedUnchanged++
11611229
Logger.d(TAG, " ⏭️ Skipping $noteId: E-Tag match (content unchanged)")
11621230
processedIds.add(noteId)
11631231
continue
11641232
}
11651233

1234+
// If file doesn't exist locally, always download
1235+
if (!fileExistsLocally) {
1236+
Logger.d(TAG, " 📥 File missing locally - forcing download")
1237+
}
1238+
11661239
// 🐛 DEBUG: Log download reason
11671240
val downloadReason = when {
11681241
lastSyncTime == 0L -> "First sync ever"
@@ -1179,28 +1252,9 @@ class WebDavSyncService(private val context: Context) {
11791252
val jsonContent = sardine.get(noteUrl).bufferedReader().use { it.readText() }
11801253
val remoteNote = Note.fromJson(jsonContent) ?: continue
11811254

1182-
// NEW: Check if note was deleted locally
1183-
if (deletionTracker.isDeleted(remoteNote.id)) {
1184-
val deletedAt = deletionTracker.getDeletionTimestamp(remoteNote.id)
1185-
1186-
// Smart check: Was note re-created on server after deletion?
1187-
if (deletedAt != null && remoteNote.updatedAt > deletedAt) {
1188-
Logger.d(TAG, " 📝 Note re-created on server after deletion: ${remoteNote.id}")
1189-
deletionTracker.removeDeletion(remoteNote.id)
1190-
trackerModified = true
1191-
// Continue with download below
1192-
} else {
1193-
Logger.d(TAG, " ⏭️ Skipping deleted note: ${remoteNote.id}")
1194-
skippedDeleted++
1195-
processedIds.add(remoteNote.id)
1196-
continue
1197-
}
1198-
}
1199-
12001255
processedIds.add(remoteNote.id) // 🆕 Mark as processed
12011256

1202-
val localNote = storage.loadNote(remoteNote.id)
1203-
1257+
// Note: localNote was already loaded above for existence check
12041258
when {
12051259
localNote == null -> {
12061260
// New note from server

android/app/src/main/res/values-de/strings.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,21 @@
9393
<string name="snackbar_server_error">Server-Fehler: %s</string>
9494
<string name="snackbar_already_synced">Bereits synchronisiert</string>
9595
<string name="snackbar_server_unreachable">Server nicht erreichbar</string>
96+
<string name="snackbar_connection_timeout">Verbindungs-Timeout</string>
9697
<string name="snackbar_synced_count">✅ Gesynct: %d Notizen</string>
9798
<string name="snackbar_nothing_to_sync">ℹ️ Nichts zu syncen</string>
9899

100+
<!-- ============================= -->
101+
<!-- SYNC ERROR MESSAGES -->
102+
<!-- ============================= -->
103+
<string name="sync_error_ssl">SSL-Fehler</string>
104+
<string name="sync_error_auth_failed">Authentifizierung fehlgeschlagen</string>
105+
<string name="sync_error_access_denied">Zugriff verweigert</string>
106+
<string name="sync_error_path_not_found">Server-Pfad nicht gefunden</string>
107+
<string name="sync_error_server">Server-Fehler</string>
108+
<string name="sync_error_http">HTTP-Fehler: %d</string>
109+
<string name="sync_error_unknown">Unbekannter Fehler</string>
110+
99111
<!-- ============================= -->
100112
<!-- URL VALIDATION ERRORS -->
101113
<!-- ============================= -->

android/app/src/main/res/values/strings.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,21 @@
9393
<string name="snackbar_server_error">Server error: %s</string>
9494
<string name="snackbar_already_synced">Already synced</string>
9595
<string name="snackbar_server_unreachable">Server not reachable</string>
96+
<string name="snackbar_connection_timeout">Connection timeout</string>
9697
<string name="snackbar_synced_count">✅ Synced: %d notes</string>
9798
<string name="snackbar_nothing_to_sync">ℹ️ Nothing to sync</string>
9899

100+
<!-- ============================= -->
101+
<!-- SYNC ERROR MESSAGES -->
102+
<!-- ============================= -->
103+
<string name="sync_error_ssl">SSL error</string>
104+
<string name="sync_error_auth_failed">Authentication failed</string>
105+
<string name="sync_error_access_denied">Access denied</string>
106+
<string name="sync_error_path_not_found">Server path not found</string>
107+
<string name="sync_error_server">Server error</string>
108+
<string name="sync_error_http">HTTP error: %d</string>
109+
<string name="sync_error_unknown">Unknown error</string>
110+
99111
<!-- ============================= -->
100112
<!-- URL VALIDATION ERRORS -->
101113
<!-- ============================= -->
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
• Behoben: App-Absturz auf Android 9 - Danke an @roughnecks
2+
• Behoben: Kernel-VPN-Kompatibilität (Wireguard)
23
• Verbessert: Stabilität der Sync-Sessions
34
• Technisch: Optimierte Verbindungsverwaltung

0 commit comments

Comments
 (0)