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
This commit is contained in:
@@ -8,6 +8,33 @@ Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
---
|
||||
|
||||
## [1.7.1] - 2026-01-30
|
||||
|
||||
### 🐛 Kritische Fehlerbehebungen
|
||||
|
||||
- **App-Absturz auf Android 9 nach längerer Nutzung behoben** ([ref #15](https://github.com/inventory69/simple-notes-sync/issues/15))
|
||||
- Ressourcenerschöpfung durch nicht geschlossene HTTP-Verbindungen behoben
|
||||
- App konnte nach ~30-45 Minuten Nutzung durch angesammelte Connection-Leaks abstürzen
|
||||
- Danke an [@roughnecks] für den detaillierten Fehlerbericht!
|
||||
|
||||
- **VPN-Kompatibilitäts-Regression behoben** ([ref #11](https://github.com/inventory69/simple-notes-sync/issues/11))
|
||||
- WiFi Socket-Binding erkennt jetzt korrekt Wireguard VPN-Interfaces (tun*, wg*, *-wg-*)
|
||||
- Traffic wird korrekt durch VPN-Tunnel geleitet statt direkt über WiFi
|
||||
- Behebt "Verbindungs-Timeout" beim Sync zu externen Servern über VPN
|
||||
|
||||
### 🔧 Technische Änderungen
|
||||
|
||||
- Neue `SafeSardineWrapper` Klasse stellt korrektes HTTP-Connection-Cleanup sicher
|
||||
- Weniger unnötige 401-Authentifizierungs-Challenges durch preemptive Auth-Header
|
||||
- ProGuard-Regel hinzugefügt um harmlose TextInclusionStrategy-Warnungen zu unterdrücken
|
||||
- VPN-Interface-Erkennung via `NetworkInterface.getNetworkInterfaces()` Pattern-Matching
|
||||
|
||||
### 🌍 Lokalisierung
|
||||
|
||||
- Hardcodierte deutsche Fehlermeldungen behoben - jetzt String-Resources für korrekte Lokalisierung
|
||||
|
||||
---
|
||||
|
||||
## [1.7.0] - 2026-01-26
|
||||
|
||||
### 🎉 Major: Grid-Ansicht, Nur-WLAN Sync & VPN-Unterstützung
|
||||
|
||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -17,11 +17,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- App could crash after ~30-45 minutes of use due to accumulated connection leaks
|
||||
- Thanks to [@roughnecks] for the detailed bug report!
|
||||
|
||||
- **Fixed VPN compatibility regression** ([ref #11](https://github.com/inventory69/simple-notes-sync/issues/11))
|
||||
- WiFi socket binding now correctly detects Wireguard VPN interfaces (tun*, wg*, *-wg-*)
|
||||
- Traffic routes through VPN tunnel instead of bypassing it directly to WiFi
|
||||
- Fixes "Connection timeout" when syncing to external servers via VPN
|
||||
|
||||
### 🔧 Technical Changes
|
||||
|
||||
- New `SafeSardineWrapper` class ensures proper HTTP connection cleanup
|
||||
- Reduced unnecessary 401 authentication challenges with preemptive auth headers
|
||||
- Added ProGuard rule to suppress harmless TextInclusionStrategy warnings on older Android versions
|
||||
- VPN interface detection via `NetworkInterface.getNetworkInterfaces()` pattern matching
|
||||
|
||||
### 🌍 Localization
|
||||
|
||||
- Fixed hardcoded German error messages - now uses string resources for proper localization
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -392,13 +392,13 @@ class MainActivity : AppCompatActivity() {
|
||||
// 🔥 v1.1.2: Check if there are unsynced changes first (performance optimization)
|
||||
if (!syncService.hasUnsyncedChanges()) {
|
||||
Logger.d(TAG, "⏭️ No unsynced changes, skipping server reachability check")
|
||||
SyncStateManager.markCompleted("Bereits synchronisiert")
|
||||
SyncStateManager.markCompleted(getString(R.string.snackbar_already_synced))
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Check if server is reachable
|
||||
if (!syncService.isServerReachable()) {
|
||||
SyncStateManager.markError("Server nicht erreichbar")
|
||||
SyncStateManager.markError(getString(R.string.snackbar_server_unreachable))
|
||||
return@launch
|
||||
}
|
||||
|
||||
@@ -406,7 +406,7 @@ class MainActivity : AppCompatActivity() {
|
||||
val result = syncService.syncNotes()
|
||||
|
||||
if (result.isSuccess) {
|
||||
SyncStateManager.markCompleted("${result.syncedCount} Notizen")
|
||||
SyncStateManager.markCompleted(getString(R.string.snackbar_synced_count, result.syncedCount))
|
||||
loadNotes()
|
||||
} else {
|
||||
SyncStateManager.markError(result.errorMessage)
|
||||
@@ -683,7 +683,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
if (!isReachable) {
|
||||
Logger.d(TAG, "⏭️ Manual Sync: Server not reachable - aborting")
|
||||
SyncStateManager.markError("Server nicht erreichbar")
|
||||
SyncStateManager.markError(getString(R.string.snackbar_server_unreachable))
|
||||
return@launch
|
||||
}
|
||||
|
||||
|
||||
@@ -608,8 +608,8 @@ class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
// ⭐ WICHTIG: Server-Erreichbarkeits-Check VOR Sync (wie in anderen Triggern)
|
||||
if (!syncService.isServerReachable()) {
|
||||
showToast("⚠️ Server nicht erreichbar")
|
||||
SyncStateManager.markError("Server nicht erreichbar")
|
||||
showToast("⚠️ ${getString(R.string.snackbar_server_unreachable)}")
|
||||
SyncStateManager.markError(getString(R.string.snackbar_server_unreachable))
|
||||
checkServerStatus() // Server-Status aktualisieren
|
||||
return@launch
|
||||
}
|
||||
|
||||
@@ -104,8 +104,45 @@ class WebDavSyncService(private val context: Context) {
|
||||
return sessionWifiAddress
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔒 v1.7.1: Checks if any VPN/Wireguard interface is active.
|
||||
*
|
||||
* Wireguard VPNs run as separate network interfaces (tun*, wg*, *-wg-*),
|
||||
* and are NOT detected via NetworkCapabilities.TRANSPORT_VPN!
|
||||
*
|
||||
* @return true if VPN interface is detected
|
||||
*/
|
||||
private fun isVpnInterfaceActive(): Boolean {
|
||||
try {
|
||||
val interfaces = NetworkInterface.getNetworkInterfaces() ?: return false
|
||||
while (interfaces.hasMoreElements()) {
|
||||
val iface = interfaces.nextElement()
|
||||
if (!iface.isUp) continue
|
||||
|
||||
val name = iface.name.lowercase()
|
||||
// Check for VPN/Wireguard interface patterns:
|
||||
// - tun0, tun1, etc. (OpenVPN, generic VPN)
|
||||
// - wg0, wg1, etc. (Wireguard)
|
||||
// - *-wg-* (Mullvad, ProtonVPN style: se-sto-wg-202)
|
||||
if (name.startsWith("tun") ||
|
||||
name.startsWith("wg") ||
|
||||
name.contains("-wg-") ||
|
||||
name.startsWith("ppp")) {
|
||||
Logger.d(TAG, "🔒 VPN interface detected: ${iface.name}")
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.w(TAG, "⚠️ Failed to check VPN interfaces: ${e.message}")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet WiFi Interface IP-Adresse (um VPN zu umgehen)
|
||||
*
|
||||
* 🔒 v1.7.1 Fix: Now detects Wireguard VPN interfaces and skips WiFi binding
|
||||
* when VPN is active, so traffic routes through VPN tunnel correctly.
|
||||
*/
|
||||
@Suppress("ReturnCount") // Early returns for network validation checks
|
||||
private fun getWiFiInetAddressInternal(): InetAddress? {
|
||||
@@ -129,10 +166,17 @@ class WebDavSyncService(private val context: Context) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 🔒 v1.7.0: VPN-Detection - Skip WiFi binding when VPN is active
|
||||
// 🔒 v1.7.0: VPN-Detection via NetworkCapabilities (standard Android VPN)
|
||||
// When VPN is active, traffic should route through VPN, not directly via WiFi
|
||||
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
|
||||
Logger.d(TAG, "🔒 VPN detected - using default routing (traffic will go through VPN)")
|
||||
Logger.d(TAG, "🔒 VPN detected (TRANSPORT_VPN) - using default routing")
|
||||
return null
|
||||
}
|
||||
|
||||
// 🔒 v1.7.1: VPN-Detection via interface names (Wireguard, OpenVPN, etc.)
|
||||
// Wireguard VPNs are NOT detected via TRANSPORT_VPN, they run as separate interfaces!
|
||||
if (isVpnInterfaceActive()) {
|
||||
Logger.d(TAG, "🔒 VPN interface detected - skip WiFi binding, use default routing")
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -142,7 +186,7 @@ class WebDavSyncService(private val context: Context) {
|
||||
return null
|
||||
}
|
||||
|
||||
Logger.d(TAG, "✅ Network is WiFi, searching for interface...")
|
||||
Logger.d(TAG, "✅ Network is WiFi (no VPN), searching for interface...")
|
||||
|
||||
@Suppress("LoopWithTooManyJumpStatements") // Network interface filtering requires multiple conditions
|
||||
// Finde WiFi Interface
|
||||
@@ -649,19 +693,19 @@ class WebDavSyncService(private val context: Context) {
|
||||
SyncResult(
|
||||
isSuccess = false,
|
||||
errorMessage = when (e) {
|
||||
is java.net.UnknownHostException -> "Server nicht erreichbar"
|
||||
is java.net.SocketTimeoutException -> "Verbindungs-Timeout"
|
||||
is javax.net.ssl.SSLException -> "SSL-Fehler"
|
||||
is java.net.UnknownHostException -> context.getString(R.string.snackbar_server_unreachable)
|
||||
is java.net.SocketTimeoutException -> context.getString(R.string.snackbar_connection_timeout)
|
||||
is javax.net.ssl.SSLException -> context.getString(R.string.sync_error_ssl)
|
||||
is com.thegrizzlylabs.sardineandroid.impl.SardineException -> {
|
||||
when (e.statusCode) {
|
||||
401 -> "Authentifizierung fehlgeschlagen"
|
||||
403 -> "Zugriff verweigert"
|
||||
404 -> "Server-Pfad nicht gefunden"
|
||||
500 -> "Server-Fehler"
|
||||
else -> "HTTP-Fehler: ${e.statusCode}"
|
||||
401 -> context.getString(R.string.sync_error_auth_failed)
|
||||
403 -> context.getString(R.string.sync_error_access_denied)
|
||||
404 -> context.getString(R.string.sync_error_path_not_found)
|
||||
500 -> context.getString(R.string.sync_error_server)
|
||||
else -> context.getString(R.string.sync_error_http, e.statusCode)
|
||||
}
|
||||
}
|
||||
else -> e.message ?: "Unbekannter Fehler"
|
||||
else -> e.message ?: context.getString(R.string.sync_error_unknown)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -824,19 +868,19 @@ class WebDavSyncService(private val context: Context) {
|
||||
SyncResult(
|
||||
isSuccess = false,
|
||||
errorMessage = when (e) {
|
||||
is java.net.UnknownHostException -> "Server nicht erreichbar: ${e.message}"
|
||||
is java.net.SocketTimeoutException -> "Verbindungs-Timeout: ${e.message}"
|
||||
is javax.net.ssl.SSLException -> "SSL-Fehler"
|
||||
is java.net.UnknownHostException -> "${context.getString(R.string.snackbar_server_unreachable)}: ${e.message}"
|
||||
is java.net.SocketTimeoutException -> "${context.getString(R.string.snackbar_connection_timeout)}: ${e.message}"
|
||||
is javax.net.ssl.SSLException -> context.getString(R.string.sync_error_ssl)
|
||||
is com.thegrizzlylabs.sardineandroid.impl.SardineException -> {
|
||||
when (e.statusCode) {
|
||||
401 -> "Authentifizierung fehlgeschlagen"
|
||||
403 -> "Zugriff verweigert"
|
||||
404 -> "Server-Pfad nicht gefunden"
|
||||
500 -> "Server-Fehler"
|
||||
else -> "HTTP-Fehler: ${e.statusCode}"
|
||||
401 -> context.getString(R.string.sync_error_auth_failed)
|
||||
403 -> context.getString(R.string.sync_error_access_denied)
|
||||
404 -> context.getString(R.string.sync_error_path_not_found)
|
||||
500 -> context.getString(R.string.sync_error_server)
|
||||
else -> context.getString(R.string.sync_error_http, e.statusCode)
|
||||
}
|
||||
}
|
||||
else -> e.message ?: "Unbekannter Fehler"
|
||||
else -> e.message ?: context.getString(R.string.sync_error_unknown)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1145,9 +1189,32 @@ class WebDavSyncService(private val context: Context) {
|
||||
"modified=$serverModified lastSync=$lastSyncTime"
|
||||
)
|
||||
|
||||
// FIRST: Check deletion tracker - if locally deleted, skip unless re-created on server
|
||||
if (deletionTracker.isDeleted(noteId)) {
|
||||
val deletedAt = deletionTracker.getDeletionTimestamp(noteId)
|
||||
|
||||
// Smart check: Was note re-created on server after deletion?
|
||||
if (deletedAt != null && serverModified > deletedAt) {
|
||||
Logger.d(TAG, " 📝 Note re-created on server after deletion: $noteId")
|
||||
deletionTracker.removeDeletion(noteId)
|
||||
trackerModified = true
|
||||
// Continue with download below
|
||||
} else {
|
||||
Logger.d(TAG, " ⏭️ Skipping deleted note: $noteId")
|
||||
skippedDeleted++
|
||||
processedIds.add(noteId)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Check if file exists locally
|
||||
val localNote = storage.loadNote(noteId)
|
||||
val fileExistsLocally = localNote != null
|
||||
|
||||
// PRIMARY: Timestamp check (works on first sync!)
|
||||
// Same logic as Markdown sync - skip if not modified since last sync
|
||||
if (!forceOverwrite && lastSyncTime > 0 && serverModified <= lastSyncTime) {
|
||||
// BUT: Always download if file doesn't exist locally!
|
||||
if (!forceOverwrite && fileExistsLocally && lastSyncTime > 0 && serverModified <= lastSyncTime) {
|
||||
skippedUnchanged++
|
||||
Logger.d(TAG, " ⏭️ Skipping $noteId: Not modified since last sync (timestamp)")
|
||||
processedIds.add(noteId)
|
||||
@@ -1156,13 +1223,19 @@ class WebDavSyncService(private val context: Context) {
|
||||
|
||||
// SECONDARY: E-Tag check (for performance after first sync)
|
||||
// Catches cases where file was re-uploaded with same content
|
||||
if (!forceOverwrite && serverETag != null && serverETag == cachedETag) {
|
||||
// BUT: Always download if file doesn't exist locally!
|
||||
if (!forceOverwrite && fileExistsLocally && serverETag != null && serverETag == cachedETag) {
|
||||
skippedUnchanged++
|
||||
Logger.d(TAG, " ⏭️ Skipping $noteId: E-Tag match (content unchanged)")
|
||||
processedIds.add(noteId)
|
||||
continue
|
||||
}
|
||||
|
||||
// If file doesn't exist locally, always download
|
||||
if (!fileExistsLocally) {
|
||||
Logger.d(TAG, " 📥 File missing locally - forcing download")
|
||||
}
|
||||
|
||||
// 🐛 DEBUG: Log download reason
|
||||
val downloadReason = when {
|
||||
lastSyncTime == 0L -> "First sync ever"
|
||||
@@ -1179,28 +1252,9 @@ class WebDavSyncService(private val context: Context) {
|
||||
val jsonContent = sardine.get(noteUrl).bufferedReader().use { it.readText() }
|
||||
val remoteNote = Note.fromJson(jsonContent) ?: continue
|
||||
|
||||
// NEW: Check if note was deleted locally
|
||||
if (deletionTracker.isDeleted(remoteNote.id)) {
|
||||
val deletedAt = deletionTracker.getDeletionTimestamp(remoteNote.id)
|
||||
|
||||
// Smart check: Was note re-created on server after deletion?
|
||||
if (deletedAt != null && remoteNote.updatedAt > deletedAt) {
|
||||
Logger.d(TAG, " 📝 Note re-created on server after deletion: ${remoteNote.id}")
|
||||
deletionTracker.removeDeletion(remoteNote.id)
|
||||
trackerModified = true
|
||||
// Continue with download below
|
||||
} else {
|
||||
Logger.d(TAG, " ⏭️ Skipping deleted note: ${remoteNote.id}")
|
||||
skippedDeleted++
|
||||
processedIds.add(remoteNote.id)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
processedIds.add(remoteNote.id) // 🆕 Mark as processed
|
||||
|
||||
val localNote = storage.loadNote(remoteNote.id)
|
||||
|
||||
// Note: localNote was already loaded above for existence check
|
||||
when {
|
||||
localNote == null -> {
|
||||
// New note from server
|
||||
|
||||
@@ -93,9 +93,21 @@
|
||||
<string name="snackbar_server_error">Server-Fehler: %s</string>
|
||||
<string name="snackbar_already_synced">Bereits synchronisiert</string>
|
||||
<string name="snackbar_server_unreachable">Server nicht erreichbar</string>
|
||||
<string name="snackbar_connection_timeout">Verbindungs-Timeout</string>
|
||||
<string name="snackbar_synced_count">✅ Gesynct: %d Notizen</string>
|
||||
<string name="snackbar_nothing_to_sync">ℹ️ Nichts zu syncen</string>
|
||||
|
||||
<!-- ============================= -->
|
||||
<!-- SYNC ERROR MESSAGES -->
|
||||
<!-- ============================= -->
|
||||
<string name="sync_error_ssl">SSL-Fehler</string>
|
||||
<string name="sync_error_auth_failed">Authentifizierung fehlgeschlagen</string>
|
||||
<string name="sync_error_access_denied">Zugriff verweigert</string>
|
||||
<string name="sync_error_path_not_found">Server-Pfad nicht gefunden</string>
|
||||
<string name="sync_error_server">Server-Fehler</string>
|
||||
<string name="sync_error_http">HTTP-Fehler: %d</string>
|
||||
<string name="sync_error_unknown">Unbekannter Fehler</string>
|
||||
|
||||
<!-- ============================= -->
|
||||
<!-- URL VALIDATION ERRORS -->
|
||||
<!-- ============================= -->
|
||||
|
||||
@@ -93,9 +93,21 @@
|
||||
<string name="snackbar_server_error">Server error: %s</string>
|
||||
<string name="snackbar_already_synced">Already synced</string>
|
||||
<string name="snackbar_server_unreachable">Server not reachable</string>
|
||||
<string name="snackbar_connection_timeout">Connection timeout</string>
|
||||
<string name="snackbar_synced_count">✅ Synced: %d notes</string>
|
||||
<string name="snackbar_nothing_to_sync">ℹ️ Nothing to sync</string>
|
||||
|
||||
<!-- ============================= -->
|
||||
<!-- SYNC ERROR MESSAGES -->
|
||||
<!-- ============================= -->
|
||||
<string name="sync_error_ssl">SSL error</string>
|
||||
<string name="sync_error_auth_failed">Authentication failed</string>
|
||||
<string name="sync_error_access_denied">Access denied</string>
|
||||
<string name="sync_error_path_not_found">Server path not found</string>
|
||||
<string name="sync_error_server">Server error</string>
|
||||
<string name="sync_error_http">HTTP error: %d</string>
|
||||
<string name="sync_error_unknown">Unknown error</string>
|
||||
|
||||
<!-- ============================= -->
|
||||
<!-- URL VALIDATION ERRORS -->
|
||||
<!-- ============================= -->
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
• Behoben: App-Absturz auf Android 9 - Danke an @roughnecks
|
||||
• Behoben: Kernel-VPN-Kompatibilität (Wireguard)
|
||||
• Verbessert: Stabilität der Sync-Sessions
|
||||
• Technisch: Optimierte Verbindungsverwaltung
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
• Fixed: App crash on Android 9 - Thanks to @roughnecks
|
||||
• Fixed: Kernel-VPN compatibility (Wireguard)
|
||||
• Improved: Stability at sync sessions
|
||||
• Technical: Optimized connection management
|
||||
|
||||
Reference in New Issue
Block a user