mirror of
https://github.com/immich-app/immich.git
synced 2026-01-27 15:26:31 -08:00
refactor
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
23
mobile/lib/platform/remote_image_api.g.dart
generated
23
mobile/lib/platform/remote_image_api.g.dart
generated
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,4 @@ abstract class RemoteImageApi {
|
||||
});
|
||||
|
||||
void cancelRequest(int requestId);
|
||||
|
||||
void releaseImage(int requestId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user