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:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user