From e739aa166775a7e321c04c48716f2a454eee6b78 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Sat, 17 Jan 2026 02:31:35 -0500 Subject: [PATCH] conditional cronet --- mobile/android/app/build.gradle | 1 + .../app/alextran/immich/core/SSLConfig.kt | 7 ++- .../immich/images/RemoteImagesImpl.kt | 43 ++++++++++++++++--- .../ios/Runner/Images/RemoteImagesImpl.swift | 3 ++ mobile/packages/ui/pubspec.lock | 4 +- mobile/pubspec.lock | 42 ++++++++++++++---- mobile/pubspec.yaml | 4 +- 7 files changed, 83 insertions(+), 21 deletions(-) diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 6f0f21aa32..7ea6f4e2da 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -109,6 +109,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "com.squareup.okhttp3:okhttp:$okhttp_version" + implementation "com.google.net.cronet:cronet-okhttp:0.1.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.concurrent:concurrent-futures:$concurrent_version" diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/SSLConfig.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/SSLConfig.kt index b0327a1b3c..9686d49c2c 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/SSLConfig.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/SSLConfig.kt @@ -19,6 +19,9 @@ object SSLConfig { var trustManager: X509TrustManager? = null private set + var requiresCustomSSL: Boolean = false + private set + private val listeners = mutableListOf<() -> Unit>() private var configHash: Int = 0 @@ -34,7 +37,8 @@ object SSLConfig { clientCertHash: Int ) { val newHash = computeHash(allowSelfSigned, serverHost, clientCertHash) - if (newHash == configHash && sslSocketFactory != null) { + val newRequiresCustomSSL = allowSelfSigned || keyManagers != null + if (newHash == configHash && sslSocketFactory != null && requiresCustomSSL == newRequiresCustomSSL) { return // Config unchanged, skip } @@ -43,6 +47,7 @@ object SSLConfig { sslSocketFactory = sslContext.socketFactory trustManager = trustManagers?.filterIsInstance()?.firstOrNull() ?: getDefaultTrustManager() + requiresCustomSSL = newRequiresCustomSSL configHash = newHash notifyListeners() } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt index e24ca92bb4..1709d822c7 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt @@ -8,6 +8,7 @@ import android.graphics.ImageDecoder import android.os.Build import android.os.CancellationSignal import app.alextran.immich.core.SSLConfig +import com.google.net.cronet.okhttptransport.CronetCallFactory import okhttp3.Call import okhttp3.Callback import okhttp3.OkHttpClient @@ -16,6 +17,7 @@ import okhttp3.Response import okhttp3.Cache import okhttp3.ConnectionPool import okhttp3.Dispatcher +import org.chromium.net.CronetEngine import java.io.File import java.io.IOException import java.nio.ByteBuffer @@ -33,6 +35,7 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi { private val lockedBitmaps = ConcurrentHashMap() init { + appContext = context.applicationContext cacheDir = context.cacheDir client = buildClient() } @@ -47,8 +50,10 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi { private val decodePool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() / 2 + 1) + private var appContext: Context? = null private var cacheDir: File? = null - private var client: OkHttpClient? = null + private var client: Call.Factory? = null + private var cronetEngine: CronetEngine? = null init { System.loadLibrary("native_buffer") @@ -62,15 +67,42 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi { external fun unlockBitmapPixels(bitmap: Bitmap) private fun invalidateClient() { - client?.let { + (client as? OkHttpClient)?.let { it.dispatcher.cancelAll() it.connectionPool.evictAll() it.cache?.close() } + cronetEngine?.shutdown() + cronetEngine = null + client = buildClient() } - private fun buildClient(): OkHttpClient { + private fun buildClient(): Call.Factory { + val dir = cacheDir ?: throw IllegalStateException("Cache dir not set") + return if (SSLConfig.requiresCustomSSL) { + buildOkHttpClient(dir) + } else { + buildCronetClient(dir) + } + } + + private fun buildCronetClient(cacheDir: File): Call.Factory { + val ctx = appContext ?: throw IllegalStateException("Context not set") + val storageDir = File(cacheDir, "cronet").apply { mkdirs() } + val engine = CronetEngine.Builder(ctx) + .enableHttp2(true) + .enableQuic(true) + .enableBrotli(true) + .setStoragePath(storageDir.absolutePath) + .build() + .also { cronetEngine = it } + + return CronetCallFactory.newBuilder(engine).build() + } + + private fun buildOkHttpClient(cacheDir: File): OkHttpClient { + val dir = File(cacheDir, "okhttp") val connectionPool = ConnectionPool( maxIdleConnections = KEEP_ALIVE_CONNECTIONS, keepAliveDuration = KEEP_ALIVE_DURATION_MINUTES, @@ -81,10 +113,7 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi { .dispatcher(Dispatcher().apply { maxRequestsPerHost = MAX_REQUESTS_PER_HOST }) .connectionPool(connectionPool) - cacheDir?.let { dir -> - val cacheSubdir = File(dir, "thumbnails") - builder.cache(Cache(cacheSubdir, CACHE_SIZE_BYTES)) - } + builder.cache(Cache((File(dir, "thumbnails")), CACHE_SIZE_BYTES)) val sslSocketFactory = SSLConfig.sslSocketFactory val trustManager = SSLConfig.trustManager diff --git a/mobile/ios/Runner/Images/RemoteImagesImpl.swift b/mobile/ios/Runner/Images/RemoteImagesImpl.swift index 008c4100f4..78b6cca996 100644 --- a/mobile/ios/Runner/Images/RemoteImagesImpl.swift +++ b/mobile/ios/Runner/Images/RemoteImagesImpl.swift @@ -110,6 +110,9 @@ class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate { defer { remove(requestId: requestId) } if let error = error { + if request.isCancelled || (error as NSError).code == NSURLErrorCancelled { + return request.completion(Self.cancelledResult) + } return request.completion(.failure(error)) } diff --git a/mobile/packages/ui/pubspec.lock b/mobile/packages/ui/pubspec.lock index b9d150f174..fa0b425230 100644 --- a/mobile/packages/ui/pubspec.lock +++ b/mobile/packages/ui/pubspec.lock @@ -34,10 +34,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" sky_engine: dependency: transitive description: flutter diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index c2c41013ca..30d92d1717 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -297,6 +297,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: ae0db647e668cbb295a3527f0938e4039e004c80099dce2f964102373f5ce0b5 + url: "https://pub.dev" + source: hosted + version: "0.19.10" code_builder: dependency: transitive description: @@ -341,10 +349,10 @@ packages: dependency: "direct main" description: name: cronet_http - sha256: "1b99ad5ae81aa9d2f12900e5f17d3681f3828629bb7f7fe7ad88076a34209840" + sha256: "1fff7f26ac0c4cda97fe2a9aa082494baee4775f167c27ba45f6c8e88571e3ab" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.7.0" crop_image: dependency: "direct main" description: @@ -381,10 +389,10 @@ packages: dependency: "direct main" description: name: cupertino_http - sha256: "72187f715837290a63479a5b0ae709f4fedad0ed6bd0441c275eceaa02d5abae" + sha256: "82cbec60c90bf785a047a9525688b6dacac444e177e1d5a5876963d3c50369e8" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" custom_lint: dependency: "direct dev" description: @@ -904,6 +912,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.8.1" + hooks: + dependency: transitive + description: + name: hooks + sha256: "5410b9f4f6c9f01e8ff0eb81c9801ea13a3c3d39f8f0b1613cda08e27eab3c18" + url: "https://pub.dev" + source: hosted + version: "0.20.5" hooks_riverpod: dependency: "direct main" description: @@ -1105,10 +1121,10 @@ packages: dependency: transitive description: name: jni - sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1 + sha256: "8706a77e94c76fe9ec9315e18949cc9479cc03af97085ca9c1077b61323ea12d" url: "https://pub.dev" source: hosted - version: "0.14.2" + version: "0.15.2" js: dependency: transitive description: @@ -1269,6 +1285,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: f8872ea6c7a50ce08db9ae280ca2b8efdd973157ce462826c82f3c3051d154ce + url: "https://pub.dev" + source: hosted + version: "0.17.2" native_video_player: dependency: "direct main" description: @@ -1306,10 +1330,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "9f034ba1eeca53ddb339bc8f4813cb07336a849cd735559b60cdc068ecce2dc7" + sha256: "55eb67ede1002d9771b3f9264d2c9d30bc364f0267bc1c6cc0883280d5f0c7cb" url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "9.2.2" octo_image: dependency: "direct main" description: @@ -2235,5 +2259,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" + dart: ">=3.10.0 <4.0.0" flutter: ">=3.35.7" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 0263e2c9ad..df75b9fc15 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -86,8 +86,8 @@ dependencies: uuid: ^4.5.1 wakelock_plus: ^1.3.0 worker_manager: ^7.2.7 - cronet_http: ^1.5.0 - cupertino_http: ^2.3.0 + cronet_http: ^1.7.0 + cupertino_http: ^2.4.0 dev_dependencies: auto_route_generator: ^9.0.0