Skip to content

Commit 68e8490

Browse files
committed
Fix connection leaks causing crash on Android 9
- Added SafeSardineWrapper to properly close HTTP responses - Prevents resource exhaustion after extended use (30-45 min) - Added preemptive authentication to reduce 401 round-trips - Added ProGuard rule for TextInclusionStrategy warnings - Updated version to 1.7.1 Refs: #15
1 parent 614650e commit 68e8490

7 files changed

Lines changed: 146 additions & 15 deletions

File tree

CHANGELOG.md

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

99
---
1010

11+
## [1.7.1] - 2026-01-30
12+
13+
### 🐛 Critical Bug Fixes
14+
15+
- **Fixed app crash on Android 9 after extended use** ([ref #15](https://github.com/inventory69/simple-notes-sync/issues/15))
16+
- Fixed resource exhaustion caused by unclosed HTTP connections
17+
- App could crash after ~30-45 minutes of use due to accumulated connection leaks
18+
- Thanks to [@roughnecks] for the detailed bug report!
19+
20+
### 🔧 Technical Changes
21+
22+
- New `SafeSardineWrapper` class ensures proper HTTP connection cleanup
23+
- Reduced unnecessary 401 authentication challenges with preemptive auth headers
24+
- Added ProGuard rule to suppress harmless TextInclusionStrategy warnings on older Android versions
25+
26+
---
27+
1128
## [1.7.0] - 2026-01-26
1229

1330
### 🎉 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: Connection Leak Fix (Issue #15)
24+
versionName = "1.7.1" // 🔧 v1.7.1: Connection Leak 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
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+
}

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

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import android.content.Context
44
import android.net.ConnectivityManager
55
import android.net.NetworkCapabilities
66
import com.thegrizzlylabs.sardineandroid.Sardine
7-
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
87
import dev.dettmer.simplenotes.BuildConfig
98
import dev.dettmer.simplenotes.R
109
import dev.dettmer.simplenotes.models.DeletionTracker
@@ -56,7 +55,7 @@ class WebDavSyncService(private val context: Context) {
5655
private var notesDirEnsured = false // ⚡ v1.3.1: Cache für /notes/ Ordner-Existenz
5756

5857
// ⚡ v1.3.1 Performance: Session-Caches (werden am Ende von syncNotes() geleert)
59-
private var sessionSardine: Sardine? = null
58+
private var sessionSardine: SafeSardineWrapper? = null
6059
private var sessionWifiAddress: InetAddress? = null
6160
private var sessionWifiAddressChecked = false // Flag ob WiFi-Check bereits durchgeführt
6261

@@ -235,12 +234,16 @@ class WebDavSyncService(private val context: Context) {
235234

236235
/**
237236
* Erstellt einen neuen Sardine-Client (intern)
237+
*
238+
* 🔧 v1.7.1: Verwendet SafeSardineWrapper statt OkHttpSardine
239+
* - Verhindert Connection Leaks durch proper Response-Cleanup
240+
* - Preemptive Authentication für weniger 401-Round-Trips
238241
*/
239-
private fun createSardineClient(): Sardine? {
242+
private fun createSardineClient(): SafeSardineWrapper? {
240243
val username = prefs.getString(Constants.KEY_USERNAME, null) ?: return null
241244
val password = prefs.getString(Constants.KEY_PASSWORD, null) ?: return null
242245

243-
Logger.d(TAG, "🔧 Creating OkHttpSardine with WiFi binding")
246+
Logger.d(TAG, "🔧 Creating SafeSardineWrapper with WiFi binding")
244247
Logger.d(TAG, " Context: ${context.javaClass.simpleName}")
245248

246249
// ⚡ v1.3.1: Verwende gecachte WiFi-Adresse
@@ -256,9 +259,7 @@ class WebDavSyncService(private val context: Context) {
256259
OkHttpClient.Builder().build()
257260
}
258261

259-
return OkHttpSardine(okHttpClient).apply {
260-
setCredentials(username, password)
261-
}
262+
return SafeSardineWrapper.create(okHttpClient, username, password)
262263
}
263264

264265
/**
@@ -1030,9 +1031,7 @@ class WebDavSyncService(private val context: Context) {
10301031
OkHttpClient.Builder().build()
10311032
}
10321033

1033-
val sardine = OkHttpSardine(okHttpClient).apply {
1034-
setCredentials(username, password)
1035-
}
1034+
val sardine = SafeSardineWrapper.create(okHttpClient, username, password)
10361035

10371036
val mdUrl = getMarkdownUrl(serverUrl)
10381037

@@ -1544,8 +1543,8 @@ class WebDavSyncService(private val context: Context) {
15441543
return@withContext try {
15451544
Logger.d(TAG, "📝 Starting Markdown sync...")
15461545

1547-
val sardine = OkHttpSardine()
1548-
sardine.setCredentials(username, password)
1546+
val okHttpClient = OkHttpClient.Builder().build()
1547+
val sardine = SafeSardineWrapper.create(okHttpClient, username, password)
15491548

15501549
val mdUrl = getMarkdownUrl(serverUrl)
15511550

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
• Behoben: App-Absturz auf Android 9 - Danke an @roughnecks
2+
• Verbessert: Stabilität der Sync-Sessions
3+
• Technisch: Optimierte Verbindungsverwaltung
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
• Fixed: App crash on Android 9 - Thanks to @roughnecks
2+
• Improved: Stability at sync sessions
3+
• Technical: Optimized connection management

0 commit comments

Comments
 (0)