mirror of
https://github.com/immich-app/immich.git
synced 2026-03-26 20:00:44 +03:00
refactor
This commit is contained in:
@@ -2,10 +2,8 @@ package app.alextran.immich
|
|||||||
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
/**
|
const val INITIAL_BUFFER_SIZE = 32 * 1024
|
||||||
* JNI interface for native memory operations.
|
|
||||||
* Used by HTTP responses and image processing to avoid copies.
|
|
||||||
*/
|
|
||||||
object NativeBuffer {
|
object NativeBuffer {
|
||||||
init {
|
init {
|
||||||
System.loadLibrary("native_buffer")
|
System.loadLibrary("native_buffer")
|
||||||
@@ -26,3 +24,29 @@ object NativeBuffer {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
external fun copy(buffer: ByteBuffer, destAddress: Long, offset: Int, length: Int)
|
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 {
|
interface RemoteImageApi {
|
||||||
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, callback: (Result<Map<String, Long>>) -> Unit)
|
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, callback: (Result<Map<String, Long>>) -> Unit)
|
||||||
fun cancelRequest(requestId: Long)
|
fun cancelRequest(requestId: Long)
|
||||||
fun releaseImage(requestId: Long)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by RemoteImageApi. */
|
/** The codec used by RemoteImageApi. */
|
||||||
@@ -100,24 +99,6 @@ interface RemoteImageApi {
|
|||||||
channel.setMessageHandler(null)
|
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.CancellationSignal
|
||||||
import android.os.OperationCanceledException
|
import android.os.OperationCanceledException
|
||||||
import app.alextran.immich.BuildConfig
|
import app.alextran.immich.BuildConfig
|
||||||
|
import app.alextran.immich.INITIAL_BUFFER_SIZE
|
||||||
import app.alextran.immich.NativeBuffer
|
import app.alextran.immich.NativeBuffer
|
||||||
|
import app.alextran.immich.NativeByteBuffer
|
||||||
import app.alextran.immich.core.SSLConfig
|
import app.alextran.immich.core.SSLConfig
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.Call
|
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_CONNECTIONS = 10
|
||||||
private const val KEEP_ALIVE_DURATION_MINUTES = 5L
|
private const val KEEP_ALIVE_DURATION_MINUTES = 5L
|
||||||
private const val CACHE_SIZE_BYTES = 1024L * 1024 * 1024
|
private const val CACHE_SIZE_BYTES = 1024L * 1024 * 1024
|
||||||
private const val INITIAL_BUFFER_SIZE = 64 * 1024
|
|
||||||
|
|
||||||
private class RemoteRequest(
|
private class RemoteRequest(val cancellationSignal: CancellationSignal)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
||||||
private val requestMap = ConcurrentHashMap<Long, RemoteRequest>()
|
private val requestMap = ConcurrentHashMap<Long, RemoteRequest>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
appContext = context.applicationContext
|
ImageFetcherManager.initialize(context)
|
||||||
cacheDir = context.cacheDir
|
|
||||||
fetcher = buildFetcher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val CANCELLED = Result.success<Map<String, Long>>(emptyMap())
|
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(
|
override fun requestImage(
|
||||||
@@ -117,11 +55,10 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
|||||||
requestId: Long,
|
requestId: Long,
|
||||||
callback: (Result<Map<String, Long>>) -> Unit
|
callback: (Result<Map<String, Long>>) -> Unit
|
||||||
) {
|
) {
|
||||||
val fetcher = fetcher ?: return callback(Result.failure(RuntimeException("No fetcher")))
|
|
||||||
val signal = CancellationSignal()
|
val signal = CancellationSignal()
|
||||||
val request = RemoteRequest(signal)
|
requestMap[requestId] = RemoteRequest(signal)
|
||||||
requestMap[requestId] = request
|
|
||||||
fetcher.fetch(
|
ImageFetcherManager.fetch(
|
||||||
url,
|
url,
|
||||||
headers,
|
headers,
|
||||||
signal,
|
signal,
|
||||||
@@ -132,10 +69,14 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
|||||||
return@fetch callback(CANCELLED)
|
return@fetch callback(CANCELLED)
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(Result.success(mapOf(
|
callback(
|
||||||
"pointer" to buffer.pointer,
|
Result.success(
|
||||||
"length" to buffer.offset.toLong()
|
mapOf(
|
||||||
)))
|
"pointer" to buffer.pointer,
|
||||||
|
"length" to buffer.offset.toLong()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onFailure = { e ->
|
onFailure = { e ->
|
||||||
requestMap.remove(requestId)
|
requestMap.remove(requestId)
|
||||||
@@ -148,8 +89,53 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
|||||||
override fun cancelRequest(requestId: Long) {
|
override fun cancelRequest(requestId: Long) {
|
||||||
requestMap.remove(requestId)?.cancellationSignal?.cancel()
|
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 {
|
private sealed interface ImageFetcher {
|
||||||
@@ -164,10 +150,7 @@ private sealed interface ImageFetcher {
|
|||||||
fun drain()
|
fun drain()
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CronetImageFetcher(
|
private class CronetImageFetcher(context: Context, cacheDir: File) : ImageFetcher {
|
||||||
context: Context,
|
|
||||||
cacheDir: File,
|
|
||||||
) : ImageFetcher {
|
|
||||||
private val engine: CronetEngine
|
private val engine: CronetEngine
|
||||||
private val executor = Executors.newSingleThreadExecutor()
|
private val executor = Executors.newSingleThreadExecutor()
|
||||||
private val stateLock = Any()
|
private val stateLock = Any()
|
||||||
@@ -256,7 +239,11 @@ private class CronetImageFetcher(
|
|||||||
request.read(buffer!!.wrapRemaining())
|
request.read(buffer!!.wrapRemaining())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
|
override fun onReadCompleted(
|
||||||
|
request: UrlRequest,
|
||||||
|
info: UrlResponseInfo,
|
||||||
|
byteBuffer: ByteBuffer
|
||||||
|
) {
|
||||||
buffer!!.apply {
|
buffer!!.apply {
|
||||||
advance(byteBuffer.remaining())
|
advance(byteBuffer.remaining())
|
||||||
ensureHeadroom()
|
ensureHeadroom()
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ class RemoteImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
|
|||||||
protocol RemoteImageApi {
|
protocol RemoteImageApi {
|
||||||
func requestImage(url: String, headers: [String: String], requestId: Int64, completion: @escaping (Result<[String: Int64], Error>) -> Void)
|
func requestImage(url: String, headers: [String: String], requestId: Int64, completion: @escaping (Result<[String: Int64], Error>) -> Void)
|
||||||
func cancelRequest(requestId: Int64) throws
|
func cancelRequest(requestId: Int64) throws
|
||||||
func releaseImage(requestId: Int64) throws
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
@@ -115,20 +114,5 @@ class RemoteImageApiSetup {
|
|||||||
} else {
|
} else {
|
||||||
cancelRequestChannel.setMessageHandler(nil)
|
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) {
|
func cancelRequest(requestId: Int64) {
|
||||||
Self.delegate.cancel(requestId: requestId)
|
Self.delegate.cancel(requestId: requestId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func releaseImage(requestId: Int64) throws {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate {
|
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;
|
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 cancelRequest(int requestId);
|
||||||
|
|
||||||
void releaseImage(int requestId);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user