This commit is contained in:
mertalev
2026-01-20 16:31:27 -05:00
parent fd89607142
commit d898cba04d
7 changed files with 95 additions and 146 deletions

View File

@@ -2,10 +2,8 @@ package app.alextran.immich
import java.nio.ByteBuffer
/**
* JNI interface for native memory operations.
* Used by HTTP responses and image processing to avoid copies.
*/
const val INITIAL_BUFFER_SIZE = 32 * 1024
object NativeBuffer {
init {
System.loadLibrary("native_buffer")
@@ -26,3 +24,29 @@ object NativeBuffer {
@JvmStatic
external fun copy(buffer: ByteBuffer, destAddress: Long, offset: Int, length: Int)
}
class NativeByteBuffer(initialCapacity: Int) {
var pointer = NativeBuffer.allocate(initialCapacity)
var capacity = initialCapacity
var offset = 0
fun ensureHeadroom(needed: Int = INITIAL_BUFFER_SIZE) {
if (offset + needed > capacity) {
capacity = (capacity * 2).coerceAtLeast(offset + needed)
pointer = NativeBuffer.realloc(pointer, capacity)
}
}
fun wrapRemaining() = NativeBuffer.wrap(pointer + offset, capacity - offset)
fun advance(bytesRead: Int) {
offset += bytesRead
}
fun free() {
if (pointer != 0L) {
NativeBuffer.free(pointer)
pointer = 0L
}
}
}

View File

@@ -49,7 +49,6 @@ private open class RemoteImagesPigeonCodec : StandardMessageCodec() {
interface RemoteImageApi {
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, callback: (Result<Map<String, Long>>) -> Unit)
fun cancelRequest(requestId: Long)
fun releaseImage(requestId: Long)
companion object {
/** The codec used by RemoteImageApi. */
@@ -100,24 +99,6 @@ interface RemoteImageApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.RemoteImageApi.releaseImage$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val requestIdArg = args[0] as Long
val wrapped: List<Any?> = try {
api.releaseImage(requestIdArg)
listOf(null)
} catch (exception: Throwable) {
RemoteImagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@@ -4,7 +4,9 @@ import android.content.Context
import android.os.CancellationSignal
import android.os.OperationCanceledException
import app.alextran.immich.BuildConfig
import app.alextran.immich.INITIAL_BUFFER_SIZE
import app.alextran.immich.NativeBuffer
import app.alextran.immich.NativeByteBuffer
import app.alextran.immich.core.SSLConfig
import okhttp3.Cache
import okhttp3.Call
@@ -33,82 +35,18 @@ private const val MAX_REQUESTS_PER_HOST = 16
private const val KEEP_ALIVE_CONNECTIONS = 10
private const val KEEP_ALIVE_DURATION_MINUTES = 5L
private const val CACHE_SIZE_BYTES = 1024L * 1024 * 1024
private const val INITIAL_BUFFER_SIZE = 64 * 1024
private class RemoteRequest(
val cancellationSignal: CancellationSignal,
)
private class NativeByteBuffer(initialCapacity: Int) {
var pointer = NativeBuffer.allocate(initialCapacity)
var capacity = initialCapacity
var offset = 0
fun ensureHeadroom(needed: Int = INITIAL_BUFFER_SIZE) {
if (offset + needed > capacity) {
capacity = (capacity * 2).coerceAtLeast(offset + needed)
pointer = NativeBuffer.realloc(pointer, capacity)
}
}
fun wrapRemaining() = NativeBuffer.wrap(pointer + offset, capacity - offset)
fun advance(bytesRead: Int) { offset += bytesRead }
fun free() {
if (pointer != 0L) {
NativeBuffer.free(pointer)
pointer = 0L
}
}
}
private class RemoteRequest(val cancellationSignal: CancellationSignal)
class RemoteImagesImpl(context: Context) : RemoteImageApi {
private val requestMap = ConcurrentHashMap<Long, RemoteRequest>()
init {
appContext = context.applicationContext
cacheDir = context.cacheDir
fetcher = buildFetcher()
ImageFetcherManager.initialize(context)
}
companion object {
val CANCELLED = Result.success<Map<String, Long>>(emptyMap())
private var appContext: Context? = null
private var cacheDir: File? = null
private var fetcher: ImageFetcher? = null
init {
SSLConfig.addListener(::invalidateFetcher)
}
private fun invalidateFetcher() {
val oldFetcher = fetcher
val needsOkHttp = SSLConfig.requiresCustomSSL
fetcher = when {
// OkHttp → OkHttp: reconfigure, sharing cache/dispatcher
oldFetcher is OkHttpImageFetcher && needsOkHttp -> {
oldFetcher.reconfigure(SSLConfig.sslSocketFactory, SSLConfig.trustManager)
}
// Any other transition: graceful drain, create new
else -> {
oldFetcher?.drain()
buildFetcher()
}
}
}
private fun buildFetcher(): ImageFetcher {
val ctx = appContext ?: throw IllegalStateException("Context not set")
val dir = cacheDir ?: throw IllegalStateException("Cache dir not set")
return if (SSLConfig.requiresCustomSSL) {
OkHttpImageFetcher.create(dir, SSLConfig.sslSocketFactory, SSLConfig.trustManager)
} else {
CronetImageFetcher(ctx, dir)
}
}
}
override fun requestImage(
@@ -117,11 +55,10 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
requestId: Long,
callback: (Result<Map<String, Long>>) -> Unit
) {
val fetcher = fetcher ?: return callback(Result.failure(RuntimeException("No fetcher")))
val signal = CancellationSignal()
val request = RemoteRequest(signal)
requestMap[requestId] = request
fetcher.fetch(
requestMap[requestId] = RemoteRequest(signal)
ImageFetcherManager.fetch(
url,
headers,
signal,
@@ -132,10 +69,14 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
return@fetch callback(CANCELLED)
}
callback(Result.success(mapOf(
"pointer" to buffer.pointer,
"length" to buffer.offset.toLong()
)))
callback(
Result.success(
mapOf(
"pointer" to buffer.pointer,
"length" to buffer.offset.toLong()
)
)
)
},
onFailure = { e ->
requestMap.remove(requestId)
@@ -148,8 +89,53 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
override fun cancelRequest(requestId: Long) {
requestMap.remove(requestId)?.cancellationSignal?.cancel()
}
}
override fun releaseImage(requestId: Long) {}
private object ImageFetcherManager {
private lateinit var appContext: Context
private lateinit var cacheDir: File
private lateinit var fetcher: ImageFetcher
private var initialized = false
fun initialize(context: Context) {
if (initialized) return
synchronized(this) {
if (initialized) return
appContext = context.applicationContext
cacheDir = context.cacheDir
fetcher = build()
SSLConfig.addListener(::invalidate)
initialized = true
}
}
fun fetch(
url: String,
headers: Map<String, String>,
signal: CancellationSignal,
onSuccess: (NativeByteBuffer) -> Unit,
onFailure: (Exception) -> Unit,
) {
fetcher.fetch(url, headers, signal, onSuccess, onFailure)
}
private fun invalidate() {
val oldFetcher = fetcher
if (oldFetcher is OkHttpImageFetcher && SSLConfig.requiresCustomSSL) {
fetcher = oldFetcher.reconfigure(SSLConfig.sslSocketFactory, SSLConfig.trustManager)
return
}
fetcher = build()
oldFetcher.drain()
}
private fun build(): ImageFetcher {
return if (SSLConfig.requiresCustomSSL) {
OkHttpImageFetcher.create(cacheDir, SSLConfig.sslSocketFactory, SSLConfig.trustManager)
} else {
CronetImageFetcher(appContext, cacheDir)
}
}
}
private sealed interface ImageFetcher {
@@ -164,10 +150,7 @@ private sealed interface ImageFetcher {
fun drain()
}
private class CronetImageFetcher(
context: Context,
cacheDir: File,
) : ImageFetcher {
private class CronetImageFetcher(context: Context, cacheDir: File) : ImageFetcher {
private val engine: CronetEngine
private val executor = Executors.newSingleThreadExecutor()
private val stateLock = Any()
@@ -256,7 +239,11 @@ private class CronetImageFetcher(
request.read(buffer!!.wrapRemaining())
}
override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
override fun onReadCompleted(
request: UrlRequest,
info: UrlResponseInfo,
byteBuffer: ByteBuffer
) {
buffer!!.apply {
advance(byteBuffer.remaining())
ensureHeadroom()

View File

@@ -72,7 +72,6 @@ class RemoteImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
protocol RemoteImageApi {
func requestImage(url: String, headers: [String: String], requestId: Int64, completion: @escaping (Result<[String: Int64], Error>) -> Void)
func cancelRequest(requestId: Int64) throws
func releaseImage(requestId: Int64) throws
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -115,20 +114,5 @@ class RemoteImageApiSetup {
} else {
cancelRequestChannel.setMessageHandler(nil)
}
let releaseImageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.RemoteImageApi.releaseImage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
releaseImageChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let requestIdArg = args[0] as! Int64
do {
try api.releaseImage(requestId: requestIdArg)
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
}
}
} else {
releaseImageChannel.setMessageHandler(nil)
}
}
}

View File

@@ -50,8 +50,6 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
func cancelRequest(requestId: Int64) {
Self.delegate.cancel(requestId: requestId)
}
func releaseImage(requestId: Int64) throws {}
}
class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {

View File

@@ -103,27 +103,4 @@ class RemoteImageApi {
return;
}
}
Future<void> releaseImage(int requestId) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.RemoteImageApi.releaseImage$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[requestId]);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
}

View File

@@ -22,6 +22,4 @@ abstract class RemoteImageApi {
});
void cancelRequest(int requestId);
void releaseImage(int requestId);
}