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
This commit is contained in:
inventory69
2026-01-30 13:35:37 +01:00
parent 614650e37d
commit 68e8490db8
7 changed files with 146 additions and 15 deletions

View File

@@ -8,6 +8,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
---
## [1.7.1] - 2026-01-30
### 🐛 Critical Bug Fixes
- **Fixed app crash on Android 9 after extended use** ([ref #15](https://github.com/inventory69/simple-notes-sync/issues/15))
- Fixed resource exhaustion caused by unclosed HTTP connections
- App could crash after ~30-45 minutes of use due to accumulated connection leaks
- Thanks to [@roughnecks] for the detailed bug report!
### 🔧 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
---
## [1.7.0] - 2026-01-26
### 🎉 Major: Grid View, WiFi-Only Sync & VPN Support

View File

@@ -20,8 +20,8 @@ android {
applicationId = "dev.dettmer.simplenotes"
minSdk = 24
targetSdk = 36
versionCode = 17 // 🎨 v1.7.0: Grid Layout + Backup Encryption
versionName = "1.7.0" // 🎨 v1.7.0: Grid Layout + Backup Encryption
versionCode = 18 // 🔧 v1.7.1: Connection Leak Fix (Issue #15)
versionName = "1.7.1" // 🔧 v1.7.1: Connection Leak Fix
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -60,4 +60,8 @@
-keep class * implements com.google.gson.JsonDeserializer
# Keep your app's data classes
-keep class dev.dettmer.simplenotes.** { *; }
-keep class dev.dettmer.simplenotes.** { *; }
# v1.7.1: Suppress TextInclusionStrategy warnings on older Android versions
# This class only exists on API 35+ but Compose handles the fallback gracefully
-dontwarn android.text.Layout$TextInclusionStrategy

View File

@@ -0,0 +1,105 @@
package dev.dettmer.simplenotes.sync
import com.thegrizzlylabs.sardineandroid.DavResource
import com.thegrizzlylabs.sardineandroid.Sardine
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
import dev.dettmer.simplenotes.utils.Logger
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.InputStream
/**
* 🔧 v1.7.1: Wrapper für Sardine der Connection Leaks verhindert
*
* Hintergrund:
* - OkHttpSardine.exists() schließt den Response-Body nicht
* - Dies führt zu "connection leaked" Warnungen im Log
* - Kann bei vielen Requests zu Socket-Exhaustion führen
*
* Lösung:
* - Eigene exists()-Implementation mit korrektem Response-Cleanup
* - Preemptive Authentication um 401-Round-Trips zu vermeiden
*
* @see <a href="https://square.github.io/okhttp/4.x/okhttp/okhttp3/-response-body/">OkHttp Response Body Docs</a>
*/
class SafeSardineWrapper private constructor(
private val delegate: OkHttpSardine,
private val okHttpClient: OkHttpClient,
private val authHeader: String
) : Sardine by delegate {
companion object {
private const val TAG = "SafeSardine"
/**
* Factory-Methode für SafeSardineWrapper
*/
fun create(
okHttpClient: OkHttpClient,
username: String,
password: String
): SafeSardineWrapper {
val delegate = OkHttpSardine(okHttpClient).apply {
setCredentials(username, password)
}
val authHeader = Credentials.basic(username, password)
return SafeSardineWrapper(delegate, okHttpClient, authHeader)
}
}
/**
* ✅ Sichere exists()-Implementation mit Response Cleanup
*
* Im Gegensatz zu OkHttpSardine.exists() wird hier:
* 1. Preemptive Auth-Header gesendet (kein 401 Round-Trip)
* 2. Response.use{} für garantiertes Cleanup verwendet
*/
override fun exists(url: String): Boolean {
val request = Request.Builder()
.url(url)
.head()
.header("Authorization", authHeader)
.build()
return try {
okHttpClient.newCall(request).execute().use { response ->
val isSuccess = response.isSuccessful
Logger.d(TAG, "exists($url) → $isSuccess (${response.code})")
isSuccess
}
} catch (e: Exception) {
Logger.d(TAG, "exists($url) failed: ${e.message}")
false
}
}
/**
* ✅ Wrapper um get() mit Logging
*
* WICHTIG: Der zurückgegebene InputStream MUSS vom Caller geschlossen werden!
* Empfohlen: inputStream.bufferedReader().use { it.readText() }
*/
override fun get(url: String): InputStream {
Logger.d(TAG, "get($url)")
return delegate.get(url)
}
/**
* ✅ Wrapper um list() mit Logging
*/
override fun list(url: String): List<DavResource> {
Logger.d(TAG, "list($url)")
return delegate.list(url)
}
/**
* ✅ Wrapper um list(url, depth) mit Logging
*/
override fun list(url: String, depth: Int): List<DavResource> {
Logger.d(TAG, "list($url, depth=$depth)")
return delegate.list(url, depth)
}
// Alle anderen Methoden werden automatisch durch 'by delegate' weitergeleitet
}

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import com.thegrizzlylabs.sardineandroid.Sardine
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
import dev.dettmer.simplenotes.BuildConfig
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.DeletionTracker
@@ -56,7 +55,7 @@ class WebDavSyncService(private val context: Context) {
private var notesDirEnsured = false // ⚡ v1.3.1: Cache für /notes/ Ordner-Existenz
// ⚡ v1.3.1 Performance: Session-Caches (werden am Ende von syncNotes() geleert)
private var sessionSardine: Sardine? = null
private var sessionSardine: SafeSardineWrapper? = null
private var sessionWifiAddress: InetAddress? = null
private var sessionWifiAddressChecked = false // Flag ob WiFi-Check bereits durchgeführt
@@ -235,12 +234,16 @@ class WebDavSyncService(private val context: Context) {
/**
* Erstellt einen neuen Sardine-Client (intern)
*
* 🔧 v1.7.1: Verwendet SafeSardineWrapper statt OkHttpSardine
* - Verhindert Connection Leaks durch proper Response-Cleanup
* - Preemptive Authentication für weniger 401-Round-Trips
*/
private fun createSardineClient(): Sardine? {
private fun createSardineClient(): SafeSardineWrapper? {
val username = prefs.getString(Constants.KEY_USERNAME, null) ?: return null
val password = prefs.getString(Constants.KEY_PASSWORD, null) ?: return null
Logger.d(TAG, "🔧 Creating OkHttpSardine with WiFi binding")
Logger.d(TAG, "🔧 Creating SafeSardineWrapper with WiFi binding")
Logger.d(TAG, " Context: ${context.javaClass.simpleName}")
// ⚡ v1.3.1: Verwende gecachte WiFi-Adresse
@@ -256,9 +259,7 @@ class WebDavSyncService(private val context: Context) {
OkHttpClient.Builder().build()
}
return OkHttpSardine(okHttpClient).apply {
setCredentials(username, password)
}
return SafeSardineWrapper.create(okHttpClient, username, password)
}
/**
@@ -1030,9 +1031,7 @@ class WebDavSyncService(private val context: Context) {
OkHttpClient.Builder().build()
}
val sardine = OkHttpSardine(okHttpClient).apply {
setCredentials(username, password)
}
val sardine = SafeSardineWrapper.create(okHttpClient, username, password)
val mdUrl = getMarkdownUrl(serverUrl)
@@ -1544,8 +1543,8 @@ class WebDavSyncService(private val context: Context) {
return@withContext try {
Logger.d(TAG, "📝 Starting Markdown sync...")
val sardine = OkHttpSardine()
sardine.setCredentials(username, password)
val okHttpClient = OkHttpClient.Builder().build()
val sardine = SafeSardineWrapper.create(okHttpClient, username, password)
val mdUrl = getMarkdownUrl(serverUrl)

View File

@@ -0,0 +1,3 @@
• Behoben: App-Absturz auf Android 9 - Danke an @roughnecks
• Verbessert: Stabilität der Sync-Sessions
• Technisch: Optimierte Verbindungsverwaltung

View File

@@ -0,0 +1,3 @@
• Fixed: App crash on Android 9 - Thanks to @roughnecks
• Improved: Stability at sync sessions
• Technical: Optimized connection management