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
|
## [1.7.0] - 2026-01-26
|
||||||
|
|
||||||
### 🎉 Major: Grid View, WiFi-Only Sync & VPN Support
|
### 🎉 Major: Grid View, WiFi-Only Sync & VPN Support
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ android {
|
|||||||
applicationId = "dev.dettmer.simplenotes"
|
applicationId = "dev.dettmer.simplenotes"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 17 // 🎨 v1.7.0: Grid Layout + Backup Encryption
|
versionCode = 18 // 🔧 v1.7.1: Connection Leak Fix (Issue #15)
|
||||||
versionName = "1.7.0" // 🎨 v1.7.0: Grid Layout + Backup Encryption
|
versionName = "1.7.1" // 🔧 v1.7.1: Connection Leak Fix
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
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 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
|
||||||
|
|||||||
@@ -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.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import com.thegrizzlylabs.sardineandroid.Sardine
|
import com.thegrizzlylabs.sardineandroid.Sardine
|
||||||
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
|
|
||||||
import dev.dettmer.simplenotes.BuildConfig
|
import dev.dettmer.simplenotes.BuildConfig
|
||||||
import dev.dettmer.simplenotes.R
|
import dev.dettmer.simplenotes.R
|
||||||
import dev.dettmer.simplenotes.models.DeletionTracker
|
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
|
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)
|
// ⚡ 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 sessionWifiAddress: InetAddress? = null
|
||||||
private var sessionWifiAddressChecked = false // Flag ob WiFi-Check bereits durchgeführt
|
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)
|
* 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 username = prefs.getString(Constants.KEY_USERNAME, null) ?: return null
|
||||||
val password = prefs.getString(Constants.KEY_PASSWORD, 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}")
|
Logger.d(TAG, " Context: ${context.javaClass.simpleName}")
|
||||||
|
|
||||||
// ⚡ v1.3.1: Verwende gecachte WiFi-Adresse
|
// ⚡ v1.3.1: Verwende gecachte WiFi-Adresse
|
||||||
@@ -256,9 +259,7 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
OkHttpClient.Builder().build()
|
OkHttpClient.Builder().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
return OkHttpSardine(okHttpClient).apply {
|
return SafeSardineWrapper.create(okHttpClient, username, password)
|
||||||
setCredentials(username, password)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1030,9 +1031,7 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
OkHttpClient.Builder().build()
|
OkHttpClient.Builder().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
val sardine = OkHttpSardine(okHttpClient).apply {
|
val sardine = SafeSardineWrapper.create(okHttpClient, username, password)
|
||||||
setCredentials(username, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
val mdUrl = getMarkdownUrl(serverUrl)
|
val mdUrl = getMarkdownUrl(serverUrl)
|
||||||
|
|
||||||
@@ -1544,8 +1543,8 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
return@withContext try {
|
return@withContext try {
|
||||||
Logger.d(TAG, "📝 Starting Markdown sync...")
|
Logger.d(TAG, "📝 Starting Markdown sync...")
|
||||||
|
|
||||||
val sardine = OkHttpSardine()
|
val okHttpClient = OkHttpClient.Builder().build()
|
||||||
sardine.setCredentials(username, password)
|
val sardine = SafeSardineWrapper.create(okHttpClient, username, password)
|
||||||
|
|
||||||
val mdUrl = getMarkdownUrl(serverUrl)
|
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