From 68e8490db8e694524a55d4a0629afec3dde6c8e6 Mon Sep 17 00:00:00 2001 From: inventory69 Date: Fri, 30 Jan 2026 13:35:37 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 17 +++ android/app/build.gradle.kts | 4 +- android/app/proguard-rules.pro | 6 +- .../simplenotes/sync/SafeSardineWrapper.kt | 105 ++++++++++++++++++ .../simplenotes/sync/WebDavSyncService.kt | 23 ++-- .../metadata/android/de-DE/changelogs/18.txt | 3 + .../metadata/android/en-US/changelogs/18.txt | 3 + 7 files changed, 146 insertions(+), 15 deletions(-) create mode 100644 android/app/src/main/java/dev/dettmer/simplenotes/sync/SafeSardineWrapper.kt create mode 100644 fastlane/metadata/android/de-DE/changelogs/18.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/18.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 6000ba3..fd2554d 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 82d4c85..e6d274b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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" } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 1dde533..87335da 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -60,4 +60,8 @@ -keep class * implements com.google.gson.JsonDeserializer # Keep your app's data classes --keep class dev.dettmer.simplenotes.** { *; } \ No newline at end of file +-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 diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SafeSardineWrapper.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SafeSardineWrapper.kt new file mode 100644 index 0000000..cc8923d --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SafeSardineWrapper.kt @@ -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 OkHttp Response Body Docs + */ +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 { + Logger.d(TAG, "list($url)") + return delegate.list(url) + } + + /** + * ✅ Wrapper um list(url, depth) mit Logging + */ + override fun list(url: String, depth: Int): List { + Logger.d(TAG, "list($url, depth=$depth)") + return delegate.list(url, depth) + } + + // Alle anderen Methoden werden automatisch durch 'by delegate' weitergeleitet +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt index 5d8dbdb..3d2ccff 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt @@ -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) diff --git a/fastlane/metadata/android/de-DE/changelogs/18.txt b/fastlane/metadata/android/de-DE/changelogs/18.txt new file mode 100644 index 0000000..7d64b33 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/18.txt @@ -0,0 +1,3 @@ +• Behoben: App-Absturz auf Android 9 - Danke an @roughnecks +• Verbessert: Stabilität der Sync-Sessions +• Technisch: Optimierte Verbindungsverwaltung diff --git a/fastlane/metadata/android/en-US/changelogs/18.txt b/fastlane/metadata/android/en-US/changelogs/18.txt new file mode 100644 index 0000000..914c3b4 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/18.txt @@ -0,0 +1,3 @@ +• Fixed: App crash on Android 9 - Thanks to @roughnecks +• Improved: Stability at sync sessions +• Technical: Optimized connection management