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:
17
CHANGELOG.md
17
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
4
android/app/proguard-rules.pro
vendored
4
android/app/proguard-rules.pro
vendored
@@ -61,3 +61,7 @@
|
||||
|
||||
# Keep your app's data classes
|
||||
-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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
3
fastlane/metadata/android/de-DE/changelogs/18.txt
Normal file
3
fastlane/metadata/android/de-DE/changelogs/18.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
• Behoben: App-Absturz auf Android 9 - Danke an @roughnecks
|
||||
• Verbessert: Stabilität der Sync-Sessions
|
||||
• Technisch: Optimierte Verbindungsverwaltung
|
||||
3
fastlane/metadata/android/en-US/changelogs/18.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/18.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
• Fixed: App crash on Android 9 - Thanks to @roughnecks
|
||||
• Improved: Stability at sync sessions
|
||||
• Technical: Optimized connection management
|
||||
Reference in New Issue
Block a user