Skip to content

Commit 45f528e

Browse files
committed
merge: fix connection issues and locale bugs (v1.7.1)
## Changes • Fixed: App crash on Android 9 (Issue #15) • Fixed: German text showing despite English language setting • Improved: Sync connection stability (10s timeout) • Improved: Code quality (removed 65 lines) ## Technical Details - Increased socket timeout from 1s to 10s - Fixed hardcoded German strings in UI - Enhanced locale support with AppCompatDelegate - Removed unreliable VPN bypass code
2 parents 614650e + cb1bc46 commit 45f528e

File tree

19 files changed

+494
-213
lines changed

19 files changed

+494
-213
lines changed

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ contact_links:
99
- name: "🐛 Troubleshooting"
1010
url: https://github.com/inventory69/simple-notes-sync/blob/main/QUICKSTART.md#troubleshooting
1111
about: Häufige Probleme und Lösungen / Common issues and solutions
12+
- name: "✨ Feature Requests & Ideas"
13+
url: https://github.com/inventory69/simple-notes-sync/discussions/categories/ideas
14+
about: Diskutiere neue Features in Discussions / Discuss new features in Discussions

CHANGELOG.de.md

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

99
---
1010

11+
## [1.7.1] - 2026-02-02
12+
13+
### 🐛 Kritische Fehlerbehebungen
14+
15+
#### Android 9 App-Absturz Fix ([#15](https://github.com/inventory69/simple-notes-sync/issues/15))
16+
17+
**Problem:** App stürzte auf Android 9 (API 28) ab wenn WorkManager Expedited Work für Hintergrund-Sync verwendet wurde.
18+
19+
**Root Cause:** Wenn `setExpedited()` in WorkManager verwendet wird, muss die `CoroutineWorker` die Methode `getForegroundInfo()` implementieren um eine Foreground Service Notification zurückzugeben. Auf Android 9-11 ruft WorkManager diese Methode auf, aber die Standard-Implementierung wirft `IllegalStateException: Not implemented`.
20+
21+
**Lösung:** `getForegroundInfo()` in `SyncWorker` implementiert um eine korrekte `ForegroundInfo` mit Sync-Progress-Notification zurückzugeben.
22+
23+
**Details:**
24+
- `ForegroundInfo` mit Sync-Progress-Notification für Android 9-11 hinzugefügt
25+
- Android 10+: Setzt `FOREGROUND_SERVICE_TYPE_DATA_SYNC` für korrekte Service-Typisierung
26+
- Foreground Service Permissions in AndroidManifest.xml hinzugefügt
27+
- Notification zeigt Sync-Progress mit indeterminiertem Progress Bar
28+
- Danke an [@roughnecks](https://github.com/roughnecks) für das detaillierte Debugging!
29+
30+
#### VPN-Kompatibilitäts-Fix ([#11](https://github.com/inventory69/simple-notes-sync/issues/11))
31+
32+
- WiFi Socket-Binding erkennt jetzt korrekt Wireguard VPN-Interfaces (tun*, wg*, *-wg-*)
33+
- Traffic wird korrekt durch VPN-Tunnel geleitet statt direkt über WiFi
34+
- Behebt "Verbindungs-Timeout" beim Sync zu externen Servern über VPN
35+
36+
### 🔧 Technische Änderungen
37+
38+
- Neue `SafeSardineWrapper` Klasse stellt korrektes HTTP-Connection-Cleanup sicher
39+
- Weniger unnötige 401-Authentifizierungs-Challenges durch preemptive Auth-Header
40+
- ProGuard-Regel hinzugefügt um harmlose TextInclusionStrategy-Warnungen zu unterdrücken
41+
- VPN-Interface-Erkennung via `NetworkInterface.getNetworkInterfaces()` Pattern-Matching
42+
- Foreground Service Erkennung und Notification-System für Hintergrund-Sync-Tasks
43+
44+
### 🌍 Lokalisierung
45+
46+
- Hardcodierte deutsche Fehlermeldungen behoben - jetzt String-Resources für korrekte Lokalisierung
47+
- Deutsche und englische Strings für Sync-Progress-Notifications hinzugefügt
48+
49+
---
50+
1151
## [1.7.0] - 2026-01-26
1252

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

CHANGELOG.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,46 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
88

99
---
1010

11+
## [1.7.1] - 2026-02-02
12+
13+
### 🐛 Critical Bug Fixes
14+
15+
#### Android 9 App Crash Fix ([#15](https://github.com/inventory69/simple-notes-sync/issues/15))
16+
17+
**Problem:** App crashed on Android 9 (API 28) when using WorkManager Expedited Work for background sync.
18+
19+
**Root Cause:** When `setExpedited()` is used in WorkManager, the `CoroutineWorker` must implement `getForegroundInfo()` to return a Foreground Service notification. On Android 9-11, WorkManager calls this method, but the default implementation throws `IllegalStateException: Not implemented`.
20+
21+
**Solution:** Implemented `getForegroundInfo()` in `SyncWorker` to return a proper `ForegroundInfo` with sync progress notification.
22+
23+
**Details:**
24+
- Added `ForegroundInfo` with sync progress notification for Android 9-11
25+
- Android 10+: Sets `FOREGROUND_SERVICE_TYPE_DATA_SYNC` for proper service typing
26+
- Added Foreground Service permissions to AndroidManifest.xml
27+
- Notification shows sync progress with indeterminate progress bar
28+
- Thanks to [@roughnecks](https://github.com/roughnecks) for the detailed debugging!
29+
30+
#### VPN Compatibility Fix ([#11](https://github.com/inventory69/simple-notes-sync/issues/11))
31+
32+
- WiFi socket binding now correctly detects Wireguard VPN interfaces (tun*, wg*, *-wg-*)
33+
- Traffic routes through VPN tunnel instead of bypassing it directly to WiFi
34+
- Fixes "Connection timeout" when syncing to external servers via VPN
35+
36+
### 🔧 Technical Changes
37+
38+
- New `SafeSardineWrapper` class ensures proper HTTP connection cleanup
39+
- Reduced unnecessary 401 authentication challenges with preemptive auth headers
40+
- Added ProGuard rule to suppress harmless TextInclusionStrategy warnings on older Android versions
41+
- VPN interface detection via `NetworkInterface.getNetworkInterfaces()` pattern matching
42+
- Foreground Service detection and notification system for background sync tasks
43+
44+
### 🌍 Localization
45+
46+
- Fixed hardcoded German error messages - now uses string resources for proper localization
47+
- Added German and English strings for sync progress notifications
48+
49+
---
50+
1151
## [1.7.0] - 2026-01-26
1252

1353
### 🎉 Major: Grid View, WiFi-Only Sync & VPN Support

android/app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ android {
2020
applicationId = "dev.dettmer.simplenotes"
2121
minSdk = 24
2222
targetSdk = 36
23-
versionCode = 17 // 🎨 v1.7.0: Grid Layout + Backup Encryption
24-
versionName = "1.7.0" // 🎨 v1.7.0: Grid Layout + Backup Encryption
23+
versionCode = 18 // 🔧 v1.7.1: Android 9 getForegroundInfo Fix (Issue #15)
24+
versionName = "1.7.1" // 🔧 v1.7.1: Android 9 getForegroundInfo Fix
2525

2626
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2727
}

android/app/proguard-rules.pro

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,8 @@
6060
-keep class * implements com.google.gson.JsonDeserializer
6161

6262
# Keep your app's data classes
63-
-keep class dev.dettmer.simplenotes.** { *; }
63+
-keep class dev.dettmer.simplenotes.** { *; }
64+
65+
# v1.7.1: Suppress TextInclusionStrategy warnings on older Android versions
66+
# This class only exists on API 35+ but Compose handles the fallback gracefully
67+
-dontwarn android.text.Layout$TextInclusionStrategy

android/app/src/main/AndroidManifest.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
<!-- Battery Optimization (for WorkManager background sync) -->
1313
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
1414

15+
<!-- v1.7.1: Foreground Service for Expedited Work (Android 9-11) -->
16+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
17+
<!-- v1.7.1: Foreground Service Type for Android 10+ -->
18+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
19+
1520
<!-- BOOT Permission - CRITICAL für Auto-Sync nach Neustart! -->
1621
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
1722

@@ -91,6 +96,12 @@
9196
android:resource="@xml/file_paths" />
9297
</provider>
9398

99+
<!-- v1.7.1: WorkManager SystemForegroundService for Expedited Work -->
100+
<service
101+
android:name="androidx.work.impl.foreground.SystemForegroundService"
102+
android:foregroundServiceType="dataSync"
103+
tools:node="merge" />
104+
94105
</application>
95106

96107
</manifest>

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

Lines changed: 44 additions & 5 deletions
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()
@@ -392,21 +395,21 @@ class MainActivity : AppCompatActivity() {
392395
// 🔥 v1.1.2: Check if there are unsynced changes first (performance optimization)
393396
if (!syncService.hasUnsyncedChanges()) {
394397
Logger.d(TAG, "⏭️ No unsynced changes, skipping server reachability check")
395-
SyncStateManager.markCompleted("Bereits synchronisiert")
398+
SyncStateManager.markCompleted(getString(R.string.snackbar_already_synced))
396399
return@launch
397400
}
398401

399402
// Check if server is reachable
400403
if (!syncService.isServerReachable()) {
401-
SyncStateManager.markError("Server nicht erreichbar")
404+
SyncStateManager.markError(getString(R.string.snackbar_server_unreachable))
402405
return@launch
403406
}
404407

405408
// Perform sync
406409
val result = syncService.syncNotes()
407410

408411
if (result.isSuccess) {
409-
SyncStateManager.markCompleted("${result.syncedCount} Notizen")
412+
SyncStateManager.markCompleted(getString(R.string.snackbar_synced_count, result.syncedCount))
410413
loadNotes()
411414
} else {
412415
SyncStateManager.markError(result.errorMessage)
@@ -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

@@ -683,7 +687,7 @@ class MainActivity : AppCompatActivity() {
683687

684688
if (!isReachable) {
685689
Logger.d(TAG, "⏭️ Manual Sync: Server not reachable - aborting")
686-
SyncStateManager.markError("Server nicht erreichbar")
690+
SyncStateManager.markError(getString(R.string.snackbar_server_unreachable))
687691
return@launch
688692
}
689693

@@ -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: 3 additions & 3 deletions
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
}
@@ -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/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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package dev.dettmer.simplenotes.sync
2+
3+
import com.thegrizzlylabs.sardineandroid.DavResource
4+
import com.thegrizzlylabs.sardineandroid.Sardine
5+
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
6+
import dev.dettmer.simplenotes.utils.Logger
7+
import okhttp3.Credentials
8+
import okhttp3.OkHttpClient
9+
import okhttp3.Request
10+
import java.io.InputStream
11+
12+
/**
13+
* 🔧 v1.7.1: Wrapper für Sardine der Connection Leaks verhindert
14+
*
15+
* Hintergrund:
16+
* - OkHttpSardine.exists() schließt den Response-Body nicht
17+
* - Dies führt zu "connection leaked" Warnungen im Log
18+
* - Kann bei vielen Requests zu Socket-Exhaustion führen
19+
*
20+
* Lösung:
21+
* - Eigene exists()-Implementation mit korrektem Response-Cleanup
22+
* - Preemptive Authentication um 401-Round-Trips zu vermeiden
23+
*
24+
* @see <a href="https://square.github.io/okhttp/4.x/okhttp/okhttp3/-response-body/">OkHttp Response Body Docs</a>
25+
*/
26+
class SafeSardineWrapper private constructor(
27+
private val delegate: OkHttpSardine,
28+
private val okHttpClient: OkHttpClient,
29+
private val authHeader: String
30+
) : Sardine by delegate {
31+
32+
companion object {
33+
private const val TAG = "SafeSardine"
34+
35+
/**
36+
* Factory-Methode für SafeSardineWrapper
37+
*/
38+
fun create(
39+
okHttpClient: OkHttpClient,
40+
username: String,
41+
password: String
42+
): SafeSardineWrapper {
43+
val delegate = OkHttpSardine(okHttpClient).apply {
44+
setCredentials(username, password)
45+
}
46+
val authHeader = Credentials.basic(username, password)
47+
return SafeSardineWrapper(delegate, okHttpClient, authHeader)
48+
}
49+
}
50+
51+
/**
52+
* ✅ Sichere exists()-Implementation mit Response Cleanup
53+
*
54+
* Im Gegensatz zu OkHttpSardine.exists() wird hier:
55+
* 1. Preemptive Auth-Header gesendet (kein 401 Round-Trip)
56+
* 2. Response.use{} für garantiertes Cleanup verwendet
57+
*/
58+
override fun exists(url: String): Boolean {
59+
val request = Request.Builder()
60+
.url(url)
61+
.head()
62+
.header("Authorization", authHeader)
63+
.build()
64+
65+
return try {
66+
okHttpClient.newCall(request).execute().use { response ->
67+
val isSuccess = response.isSuccessful
68+
Logger.d(TAG, "exists($url) → $isSuccess (${response.code})")
69+
isSuccess
70+
}
71+
} catch (e: Exception) {
72+
Logger.d(TAG, "exists($url) failed: ${e.message}")
73+
false
74+
}
75+
}
76+
77+
/**
78+
* ✅ Wrapper um get() mit Logging
79+
*
80+
* WICHTIG: Der zurückgegebene InputStream MUSS vom Caller geschlossen werden!
81+
* Empfohlen: inputStream.bufferedReader().use { it.readText() }
82+
*/
83+
override fun get(url: String): InputStream {
84+
Logger.d(TAG, "get($url)")
85+
return delegate.get(url)
86+
}
87+
88+
/**
89+
* ✅ Wrapper um list() mit Logging
90+
*/
91+
override fun list(url: String): List<DavResource> {
92+
Logger.d(TAG, "list($url)")
93+
return delegate.list(url)
94+
}
95+
96+
/**
97+
* ✅ Wrapper um list(url, depth) mit Logging
98+
*/
99+
override fun list(url: String, depth: Int): List<DavResource> {
100+
Logger.d(TAG, "list($url, depth=$depth)")
101+
return delegate.list(url, depth)
102+
}
103+
104+
// Alle anderen Methoden werden automatisch durch 'by delegate' weitergeleitet
105+
}

0 commit comments

Comments
 (0)