@@ -40,7 +40,7 @@ class WebDavSyncService(private val context: Context) {
4040
4141 companion object {
4242 private const val TAG = " WebDavSyncService"
43- private const val SOCKET_TIMEOUT_MS = 1000 // 🆕 v1.7.0: Reduziert von 2s auf 1s
43+ private const val SOCKET_TIMEOUT_MS = 10000 // 🔧 v1.7.2: 10s für stabile Verbindungen (1s war zu kurz)
4444 private const val MAX_FILENAME_LENGTH = 200
4545 private const val ETAG_PREVIEW_LENGTH = 8
4646 private const val CONTENT_PREVIEW_LENGTH = 50
@@ -56,8 +56,6 @@ class WebDavSyncService(private val context: Context) {
5656
5757 // ⚡ v1.3.1 Performance: Session-Caches (werden am Ende von syncNotes() geleert)
5858 private var sessionSardine: SafeSardineWrapper ? = null
59- private var sessionWifiAddress: InetAddress ? = null
60- private var sessionWifiAddressChecked = false // Flag ob WiFi-Check bereits durchgeführt
6159
6260 init {
6361 if (BuildConfig .DEBUG ) {
@@ -89,21 +87,6 @@ class WebDavSyncService(private val context: Context) {
8987 }
9088 }
9189
92- /* *
93- * ⚡ v1.3.1: Gecachte WiFi-Adresse zurückgeben oder berechnen
94- */
95- private fun getOrCacheWiFiAddress (): InetAddress ? {
96- // Return cached if already checked this session
97- if (sessionWifiAddressChecked) {
98- return sessionWifiAddress
99- }
100-
101- // Calculate and cache
102- sessionWifiAddress = getWiFiInetAddressInternal()
103- sessionWifiAddressChecked = true
104- return sessionWifiAddress
105- }
106-
10790 /* *
10891 * 🔒 v1.7.1: Checks if any VPN/Wireguard interface is active.
10992 *
@@ -138,127 +121,6 @@ class WebDavSyncService(private val context: Context) {
138121 return false
139122 }
140123
141- /* *
142- * 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.
146- */
147- @Suppress(" ReturnCount" ) // Early returns for network validation checks
148- private fun getWiFiInetAddressInternal (): InetAddress ? {
149- try {
150- Logger .d(TAG , " 🔍 getWiFiInetAddress() called" )
151-
152- val connectivityManager = context.getSystemService(Context .CONNECTIVITY_SERVICE ) as ConnectivityManager
153- val network = connectivityManager.activeNetwork
154- Logger .d(TAG , " Active network: $network " )
155-
156- if (network == null ) {
157- Logger .d(TAG , " ❌ No active network" )
158- return null
159- }
160-
161- val capabilities = connectivityManager.getNetworkCapabilities(network)
162- Logger .d(TAG , " Network capabilities: $capabilities " )
163-
164- if (capabilities == null ) {
165- Logger .d(TAG , " ❌ No network capabilities" )
166- return null
167- }
168-
169- // 🔒 v1.7.0: VPN-Detection via NetworkCapabilities (standard Android VPN)
170- // When VPN is active, traffic should route through VPN, not directly via WiFi
171- if (capabilities.hasTransport(NetworkCapabilities .TRANSPORT_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" )
180- return null
181- }
182-
183- // Nur wenn WiFi aktiv (und kein VPN)
184- if (! capabilities.hasTransport(NetworkCapabilities .TRANSPORT_WIFI )) {
185- Logger .d(TAG , " ⚠️ Not on WiFi, using default routing" )
186- return null
187- }
188-
189- Logger .d(TAG , " ✅ Network is WiFi (no VPN), searching for interface..." )
190-
191- @Suppress(" LoopWithTooManyJumpStatements" ) // Network interface filtering requires multiple conditions
192- // Finde WiFi Interface
193- val interfaces = NetworkInterface .getNetworkInterfaces()
194- while (interfaces.hasMoreElements()) {
195- val iface = interfaces.nextElement()
196-
197- Logger .d(TAG , " Checking interface: ${iface.name} , isUp=${iface.isUp} " )
198-
199- // WiFi Interfaces: wlan0, wlan1, etc.
200- if (! iface.name.startsWith(" wlan" )) continue
201- if (! iface.isUp) continue
202-
203- val addresses = iface.inetAddresses
204- while (addresses.hasMoreElements()) {
205- val addr = addresses.nextElement()
206-
207- Logger .d(
208- TAG ,
209- " Address: ${addr.hostAddress} , IPv4=${addr is Inet4Address } , " +
210- " loopback=${addr.isLoopbackAddress} , linkLocal=${addr.isLinkLocalAddress} "
211- )
212-
213- // Nur IPv4, nicht loopback, nicht link-local
214- if (addr is Inet4Address && ! addr.isLoopbackAddress && ! addr.isLinkLocalAddress) {
215- Logger .d(TAG , " ✅ Found WiFi IP: ${addr.hostAddress} on ${iface.name} " )
216- return addr
217- }
218- }
219- }
220-
221- Logger .w(TAG , " ⚠️ No WiFi interface found, using default routing" )
222- return null
223-
224- } catch (e: Exception ) {
225- Logger .e(TAG , " ❌ Failed to get WiFi interface" , e)
226- return null
227- }
228- }
229-
230- /* *
231- * Custom SocketFactory die an WiFi-IP bindet (VPN Fix)
232- */
233- private inner class WiFiSocketFactory (private val wifiAddress : InetAddress ) : SocketFactory() {
234- override fun createSocket (): Socket {
235- val socket = Socket ()
236- socket.bind(InetSocketAddress (wifiAddress, 0 ))
237- Logger .d(TAG , " 🔌 Socket bound to WiFi IP: ${wifiAddress.hostAddress} " )
238- return socket
239- }
240-
241- override fun createSocket (host : String , port : Int ): Socket {
242- val socket = createSocket()
243- socket.connect(InetSocketAddress (host, port))
244- return socket
245- }
246-
247- override fun createSocket (host : String , port : Int , localHost : InetAddress , localPort : Int ): Socket {
248- return createSocket(host, port)
249- }
250-
251- override fun createSocket (host : InetAddress , port : Int ): Socket {
252- val socket = createSocket()
253- socket.connect(InetSocketAddress (host, port))
254- return socket
255- }
256-
257- override fun createSocket (address : InetAddress , port : Int , localAddress : InetAddress , localPort : Int ): Socket {
258- return createSocket(address, port)
259- }
260- }
261-
262124 /* *
263125 * ⚡ v1.3.1: Gecachten Sardine-Client zurückgeben oder erstellen
264126 * Spart ~100ms pro Aufruf durch Wiederverwendung
@@ -279,6 +141,10 @@ class WebDavSyncService(private val context: Context) {
279141 /* *
280142 * Erstellt einen neuen Sardine-Client (intern)
281143 *
144+ * 🆕 v1.7.2: Intelligentes Routing basierend auf Ziel-Adresse
145+ * - Lokale Server: WiFi-Binding (bypass VPN)
146+ * - Externe Server: Default-Routing (nutzt VPN wenn aktiv)
147+ *
282148 * 🔧 v1.7.1: Verwendet SafeSardineWrapper statt OkHttpSardine
283149 * - Verhindert Connection Leaks durch proper Response-Cleanup
284150 * - Preemptive Authentication für weniger 401-Round-Trips
@@ -287,21 +153,11 @@ class WebDavSyncService(private val context: Context) {
287153 val username = prefs.getString(Constants .KEY_USERNAME , null ) ? : return null
288154 val password = prefs.getString(Constants .KEY_PASSWORD , null ) ? : return null
289155
290- Logger .d(TAG , " 🔧 Creating SafeSardineWrapper with WiFi binding" )
291- Logger .d(TAG , " Context: ${context.javaClass.simpleName} " )
156+ Logger .d(TAG , " 🔧 Creating SafeSardineWrapper" )
292157
293- // ⚡ v1.3.1: Verwende gecachte WiFi-Adresse
294- val wifiAddress = getOrCacheWiFiAddress()
295-
296- val okHttpClient = if (wifiAddress != null ) {
297- Logger .d(TAG , " ✅ Using WiFi-bound socket factory" )
298- OkHttpClient .Builder ()
299- .socketFactory(WiFiSocketFactory (wifiAddress))
300- .build()
301- } else {
302- Logger .d(TAG , " ⚠️ Using default OkHttpClient (no WiFi binding)" )
303- OkHttpClient .Builder ().build()
304- }
158+ val okHttpClient = OkHttpClient .Builder ()
159+ .connectTimeout(SOCKET_TIMEOUT_MS .toLong(), java.util.concurrent.TimeUnit .MILLISECONDS )
160+ .build()
305161
306162 return SafeSardineWrapper .create(okHttpClient, username, password)
307163 }
@@ -311,8 +167,6 @@ class WebDavSyncService(private val context: Context) {
311167 */
312168 private fun clearSessionCache () {
313169 sessionSardine = null
314- sessionWifiAddress = null
315- sessionWifiAddressChecked = false
316170 notesDirEnsured = false
317171 markdownDirEnsured = false
318172 Logger .d(TAG , " 🧹 Session caches cleared" )
@@ -439,8 +293,10 @@ class WebDavSyncService(private val context: Context) {
439293 }
440294
441295 val notesUrl = getNotesUrl(serverUrl)
296+ // 🔧 v1.7.2: Exception wird NICHT gefangen - muss nach oben propagieren!
297+ // Wenn sardine.exists() timeout hat, soll hasUnsyncedChanges() das behandeln
442298 if (! sardine.exists(notesUrl)) {
443- Logger .d(TAG , " 📁 /notes/ doesn't exist - no server changes" )
299+ Logger .d(TAG , " 📁 /notes/ doesn't exist - assuming no server changes" )
444300 return false
445301 }
446302
@@ -569,8 +425,11 @@ class WebDavSyncService(private val context: Context) {
569425 hasServerChanges
570426
571427 } catch (e: Exception ) {
572- Logger .e(TAG , " Failed to check for unsynced changes" , e)
573- true // Safe default
428+ // 🔧 v1.7.2 KRITISCH: Bei Server-Fehler (Timeout, etc.) return TRUE!
429+ // Grund: Besser fälschlich synchen als "Already synced" zeigen obwohl Server nicht erreichbar
430+ Logger .e(TAG , " ❌ Failed to check server for changes: ${e.message} " )
431+ Logger .d(TAG , " ⚠️ Returning TRUE (will attempt sync) - server check failed" )
432+ true // Sicherheitshalber TRUE → Sync wird versucht und gibt dann echte Fehlermeldung
574433 }
575434 }
576435
@@ -1062,18 +921,9 @@ class WebDavSyncService(private val context: Context) {
1062921 ): Int = withContext(Dispatchers .IO ) {
1063922 Logger .d(TAG , " 🔄 Starting initial Markdown export for all notes..." )
1064923
1065- // ⚡ v1.3.1: Use cached WiFi address
1066- val wifiAddress = getOrCacheWiFiAddress()
1067-
1068- val okHttpClient = if (wifiAddress != null ) {
1069- Logger .d(TAG , " ✅ Using WiFi-bound socket factory" )
1070- OkHttpClient .Builder ()
1071- .socketFactory(WiFiSocketFactory (wifiAddress))
1072- .build()
1073- } else {
1074- Logger .d(TAG , " ⚠️ Using default OkHttpClient (no WiFi binding)" )
1075- OkHttpClient .Builder ().build()
1076- }
924+ val okHttpClient = OkHttpClient .Builder ()
925+ .connectTimeout(SOCKET_TIMEOUT_MS .toLong(), java.util.concurrent.TimeUnit .MILLISECONDS )
926+ .build()
1077927
1078928 val sardine = SafeSardineWrapper .create(okHttpClient, username, password)
1079929
0 commit comments