Skip to content

Commit 0b143e5

Browse files
committed
fix: timeout increase (1s→10s) and locale hardcoded strings
## Changes: ### Timeout Fix (v1.7.2) - SOCKET_TIMEOUT_MS: 1000ms → 10000ms for more stable connections - Better error handling in hasUnsyncedChanges(): returns TRUE on error ### Locale Fix (v1.7.2) - Replaced hardcoded German strings with getString(R.string.*) - MainActivity, SettingsActivity, MainViewModel: 'Bereits synchronisiert' → getString() - SettingsViewModel: Enhanced getString() with AppCompatDelegate locale support - Added locale debug logging in MainActivity ### Code Cleanup - Removed non-working VPN bypass code: - WiFiSocketFactory class - getWiFiInetAddressInternal() function - getOrCacheWiFiAddress() function - sessionWifiAddress cache variables - WiFi-binding logic in createSardineClient() - Kept isVpnInterfaceActive() for logging/debugging Note: VPN users should configure their VPN to exclude private IPs (e.g., 192.168.x.x) for local server connectivity. App-level VPN bypass is not reliable on Android.
1 parent cf96958 commit 0b143e5

6 files changed

Lines changed: 111 additions & 176 deletions

File tree

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ class MainActivity : AppCompatActivity() {
133133
requestNotificationPermission()
134134
}
135135

136+
// 🌍 v1.7.2: Debug Locale für Fehlersuche
137+
logLocaleInfo()
138+
136139
findViews()
137140
setupToolbar()
138141
setupRecyclerView()
@@ -672,7 +675,8 @@ class MainActivity : AppCompatActivity() {
672675
// 🔥 v1.1.2: Check if there are unsynced changes first (performance optimization)
673676
if (!syncService.hasUnsyncedChanges()) {
674677
Logger.d(TAG, "⏭️ Manual Sync: No unsynced changes - skipping")
675-
SyncStateManager.markCompleted("Bereits synchronisiert")
678+
val message = getString(R.string.toast_already_synced)
679+
SyncStateManager.markCompleted(message)
676680
return@launch
677681
}
678682

@@ -814,4 +818,39 @@ class MainActivity : AppCompatActivity() {
814818
}
815819
}
816820
}
821+
822+
/**
823+
* 🌍 v1.7.2: Debug-Logging für Locale-Problem
824+
* Hilft zu identifizieren warum deutsche Strings trotz englischer App angezeigt werden
825+
*/
826+
private fun logLocaleInfo() {
827+
if (!BuildConfig.DEBUG) return
828+
829+
Logger.d(TAG, "╔═══════════════════════════════════════════════════")
830+
Logger.d(TAG, "║ 🌍 LOCALE DEBUG INFO")
831+
Logger.d(TAG, "╠═══════════════════════════════════════════════════")
832+
833+
// System Locale
834+
val systemLocale = java.util.Locale.getDefault()
835+
Logger.d(TAG, "║ System Locale (Locale.getDefault()): $systemLocale")
836+
837+
// Resources Locale
838+
val resourcesLocale = resources.configuration.locales[0]
839+
Logger.d(TAG, "║ Resources Locale: $resourcesLocale")
840+
841+
// Context Locale (API 24+)
842+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
843+
val contextLocales = resources.configuration.locales
844+
Logger.d(TAG, "║ Context Locales (all): $contextLocales")
845+
}
846+
847+
// Test String Loading
848+
val testString = getString(R.string.toast_already_synced)
849+
Logger.d(TAG, "║ Test: getString(R.string.toast_already_synced)")
850+
Logger.d(TAG, "║ Result: '$testString'")
851+
Logger.d(TAG, "║ Expected EN: '✅ Already synced'")
852+
Logger.d(TAG, "║ Is German?: ${testString.contains("Bereits") || testString.contains("synchronisiert")}")
853+
854+
Logger.d(TAG, "╚═══════════════════════════════════════════════════")
855+
}
817856
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ class SettingsActivity : AppCompatActivity() {
599599

600600
// 🔥 v1.1.2: Check if there are unsynced changes first (performance optimization)
601601
if (!syncService.hasUnsyncedChanges()) {
602-
showToast("✅ Bereits synchronisiert")
602+
showToast(getString(R.string.toast_already_synced))
603603
SyncStateManager.markCompleted()
604604
return@launch
605605
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dev.dettmer.simplenotes
22

33
import android.app.Application
44
import android.content.Context
5+
import androidx.appcompat.app.AppCompatDelegate
56
import dev.dettmer.simplenotes.utils.Logger
67
import dev.dettmer.simplenotes.sync.NetworkMonitor
78
import dev.dettmer.simplenotes.utils.NotificationHelper
@@ -15,6 +16,18 @@ class SimpleNotesApplication : Application() {
1516

1617
lateinit var networkMonitor: NetworkMonitor // Public access für SettingsActivity
1718

19+
/**
20+
* 🌍 v1.7.1: Apply app locale to Application Context
21+
*
22+
* This ensures ViewModels and other components using Application Context
23+
* get the correct locale-specific strings.
24+
*/
25+
override fun attachBaseContext(base: Context) {
26+
// Apply the app locale before calling super
27+
// This is handled by AppCompatDelegate which reads from system storage
28+
super.attachBaseContext(base)
29+
}
30+
1831
override fun onCreate() {
1932
super.onCreate()
2033

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

Lines changed: 20 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -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

android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
536536
// Check for unsynced changes
537537
if (!syncService.hasUnsyncedChanges()) {
538538
Logger.d(TAG, "⏭️ $source Sync: No unsynced changes")
539-
SyncStateManager.markCompleted("Bereits synchronisiert")
539+
val message = getApplication<Application>().getString(R.string.toast_already_synced)
540+
SyncStateManager.markCompleted(message)
540541
loadNotes()
541542
return@launch
542543
}

0 commit comments

Comments
 (0)