mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
android impl
This commit is contained in:
@@ -105,8 +105,10 @@ dependencies {
|
|||||||
def serialization_version = '1.8.1'
|
def serialization_version = '1.8.1'
|
||||||
def compose_version = '1.1.1'
|
def compose_version = '1.1.1'
|
||||||
def gson_version = '2.10.1'
|
def gson_version = '2.10.1'
|
||||||
|
def okhttp_version = '4.12.0'
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
|
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
implementation "androidx.concurrent:concurrent-futures:$concurrent_version"
|
implementation "androidx.concurrent:concurrent-futures:$concurrent_version"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.alextran.immich
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import app.alextran.immich.core.SSLConfig
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.BinaryMessenger
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
@@ -51,15 +52,18 @@ class HttpSSLOptionsPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
when (call.method) {
|
when (call.method) {
|
||||||
"apply" -> {
|
"apply" -> {
|
||||||
val args = call.arguments<ArrayList<*>>()!!
|
val args = call.arguments<ArrayList<*>>()!!
|
||||||
|
val allowSelfSigned = args[0] as Boolean
|
||||||
|
val serverHost = args[1] as? String
|
||||||
|
val clientCertHash = (args[2] as? ByteArray)
|
||||||
|
|
||||||
var tm: Array<TrustManager>? = null
|
var tm: Array<TrustManager>? = null
|
||||||
if (args[0] as Boolean) {
|
if (allowSelfSigned) {
|
||||||
tm = arrayOf(AllowSelfSignedTrustManager(args[1] as? String))
|
tm = arrayOf(AllowSelfSignedTrustManager(serverHost))
|
||||||
}
|
}
|
||||||
|
|
||||||
var km: Array<KeyManager>? = null
|
var km: Array<KeyManager>? = null
|
||||||
if (args[2] != null) {
|
if (clientCertHash != null) {
|
||||||
val cert = ByteArrayInputStream(args[2] as ByteArray)
|
val cert = ByteArrayInputStream(clientCertHash)
|
||||||
val password = (args[3] as String).toCharArray()
|
val password = (args[3] as String).toCharArray()
|
||||||
val keyStore = KeyStore.getInstance("PKCS12")
|
val keyStore = KeyStore.getInstance("PKCS12")
|
||||||
keyStore.load(cert, password)
|
keyStore.load(cert, password)
|
||||||
@@ -69,6 +73,9 @@ class HttpSSLOptionsPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
km = keyManagerFactory.keyManagers
|
km = keyManagerFactory.keyManagers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update shared SSL config for OkHttp and other HTTP clients
|
||||||
|
SSLConfig.apply(km, tm, allowSelfSigned, serverHost, clientCertHash?.contentHashCode() ?: 0)
|
||||||
|
|
||||||
val sslContext = SSLContext.getInstance("TLS")
|
val sslContext = SSLContext.getInstance("TLS")
|
||||||
sslContext.init(km, tm, null)
|
sslContext.init(km, tm, null)
|
||||||
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
|
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import app.alextran.immich.connectivity.ConnectivityApiImpl
|
|||||||
import app.alextran.immich.core.ImmichPlugin
|
import app.alextran.immich.core.ImmichPlugin
|
||||||
import app.alextran.immich.images.LocalImageApi
|
import app.alextran.immich.images.LocalImageApi
|
||||||
import app.alextran.immich.images.LocalImagesImpl
|
import app.alextran.immich.images.LocalImagesImpl
|
||||||
|
import app.alextran.immich.images.RemoteImageApi
|
||||||
|
import app.alextran.immich.images.RemoteImagesImpl
|
||||||
import app.alextran.immich.sync.NativeSyncApi
|
import app.alextran.immich.sync.NativeSyncApi
|
||||||
import app.alextran.immich.sync.NativeSyncApiImpl26
|
import app.alextran.immich.sync.NativeSyncApiImpl26
|
||||||
import app.alextran.immich.sync.NativeSyncApiImpl30
|
import app.alextran.immich.sync.NativeSyncApiImpl30
|
||||||
@@ -37,6 +39,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
}
|
}
|
||||||
NativeSyncApi.setUp(messenger, nativeSyncApiImpl)
|
NativeSyncApi.setUp(messenger, nativeSyncApiImpl)
|
||||||
LocalImageApi.setUp(messenger, LocalImagesImpl(ctx))
|
LocalImageApi.setUp(messenger, LocalImagesImpl(ctx))
|
||||||
|
RemoteImageApi.setUp(messenger, RemoteImagesImpl(ctx))
|
||||||
|
|
||||||
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
|
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
|
||||||
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package app.alextran.immich.core
|
||||||
|
|
||||||
|
import java.security.KeyStore
|
||||||
|
import javax.net.ssl.KeyManager
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.SSLSocketFactory
|
||||||
|
import javax.net.ssl.TrustManager
|
||||||
|
import javax.net.ssl.TrustManagerFactory
|
||||||
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared SSL configuration for OkHttp and HttpsURLConnection.
|
||||||
|
* Stores the SSLSocketFactory and X509TrustManager configured by HttpSSLOptionsPlugin.
|
||||||
|
*/
|
||||||
|
object SSLConfig {
|
||||||
|
var sslSocketFactory: SSLSocketFactory? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
var trustManager: X509TrustManager? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val listeners = mutableListOf<() -> Unit>()
|
||||||
|
private var configHash: Int = 0
|
||||||
|
|
||||||
|
fun addListener(listener: () -> Unit) {
|
||||||
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun apply(
|
||||||
|
keyManagers: Array<KeyManager>?,
|
||||||
|
trustManagers: Array<TrustManager>?,
|
||||||
|
allowSelfSigned: Boolean,
|
||||||
|
serverHost: String?,
|
||||||
|
clientCertHash: Int
|
||||||
|
) {
|
||||||
|
val newHash = computeHash(allowSelfSigned, serverHost, clientCertHash)
|
||||||
|
if (newHash == configHash && sslSocketFactory != null) {
|
||||||
|
return // Config unchanged, skip
|
||||||
|
}
|
||||||
|
|
||||||
|
val sslContext = SSLContext.getInstance("TLS")
|
||||||
|
sslContext.init(keyManagers, trustManagers, null)
|
||||||
|
sslSocketFactory = sslContext.socketFactory
|
||||||
|
trustManager = trustManagers?.filterIsInstance<X509TrustManager>()?.firstOrNull()
|
||||||
|
?: getDefaultTrustManager()
|
||||||
|
configHash = newHash
|
||||||
|
notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeHash(allowSelfSigned: Boolean, serverHost: String?, clientCertHash: Int): Int {
|
||||||
|
var result = allowSelfSigned.hashCode()
|
||||||
|
result = 31 * result + (serverHost?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + clientCertHash
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyListeners() {
|
||||||
|
listeners.forEach { it() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDefaultTrustManager(): X509TrustManager {
|
||||||
|
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||||
|
factory.init(null as KeyStore?)
|
||||||
|
return factory.trustManagers.filterIsInstance<X509TrustManager>().first()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,10 @@ import android.os.OperationCanceledException
|
|||||||
import android.provider.MediaStore.Images
|
import android.provider.MediaStore.Images
|
||||||
import android.provider.MediaStore.Video
|
import android.provider.MediaStore.Video
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import app.alextran.immich.images.LocalImagesImpl.Companion.allocateNative
|
||||||
|
import app.alextran.immich.images.LocalImagesImpl.Companion.freeNative
|
||||||
|
import app.alextran.immich.images.LocalImagesImpl.Companion.wrapAsBuffer
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@@ -29,6 +33,37 @@ data class Request(
|
|||||||
val callback: (Result<Map<String, Long>>) -> Unit
|
val callback: (Result<Map<String, Long>>) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
inline fun ImageDecoder.Source.decodeBitmap(target: Size = Size(0, 0)): Bitmap {
|
||||||
|
return ImageDecoder.decodeBitmap(this) { decoder, info, _ ->
|
||||||
|
if (target.width > 0 && target.height > 0) {
|
||||||
|
val sample = max(1, min(info.size.width / target.width, info.size.height / target.height))
|
||||||
|
decoder.setTargetSampleSize(sample)
|
||||||
|
}
|
||||||
|
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
|
||||||
|
decoder.setTargetColorSpace(ColorSpace.get(ColorSpace.Named.SRGB))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bitmap.toNativeBuffer(): Map<String, Long> {
|
||||||
|
val size = width * height * 4
|
||||||
|
val pointer = allocateNative(size)
|
||||||
|
try {
|
||||||
|
val buffer = wrapAsBuffer(pointer, size)
|
||||||
|
copyPixelsToBuffer(buffer)
|
||||||
|
recycle()
|
||||||
|
return mapOf(
|
||||||
|
"pointer" to pointer,
|
||||||
|
"width" to width.toLong(),
|
||||||
|
"height" to height.toLong()
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
freeNative(pointer)
|
||||||
|
recycle()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class LocalImagesImpl(context: Context) : LocalImageApi {
|
class LocalImagesImpl(context: Context) : LocalImageApi {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
private val resolver: ContentResolver = ctx.contentResolver
|
private val resolver: ContentResolver = ctx.contentResolver
|
||||||
@@ -131,31 +166,12 @@ class LocalImagesImpl(context: Context) : LocalImageApi {
|
|||||||
decodeImage(id, size, signal)
|
decodeImage(id, size, signal)
|
||||||
}
|
}
|
||||||
|
|
||||||
processBitmap(bitmap, callback, signal)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun processBitmap(
|
|
||||||
bitmap: Bitmap, callback: (Result<Map<String, Long>>) -> Unit, signal: CancellationSignal
|
|
||||||
) {
|
|
||||||
signal.throwIfCanceled()
|
|
||||||
val actualWidth = bitmap.width
|
|
||||||
val actualHeight = bitmap.height
|
|
||||||
|
|
||||||
val size = actualWidth * actualHeight * 4
|
|
||||||
val pointer = allocateNative(size)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
signal.throwIfCanceled()
|
signal.throwIfCanceled()
|
||||||
val buffer = wrapAsBuffer(pointer, size)
|
val res = bitmap.toNativeBuffer()
|
||||||
bitmap.copyPixelsToBuffer(buffer)
|
|
||||||
bitmap.recycle()
|
|
||||||
signal.throwIfCanceled()
|
signal.throwIfCanceled()
|
||||||
val res = mapOf(
|
|
||||||
"pointer" to pointer, "width" to actualWidth.toLong(), "height" to actualHeight.toLong()
|
|
||||||
)
|
|
||||||
callback(Result.success(res))
|
callback(Result.success(res))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
freeNative(pointer)
|
|
||||||
callback(if (e is OperationCanceledException) CANCELLED else Result.failure(e))
|
callback(if (e is OperationCanceledException) CANCELLED else Result.failure(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,16 +207,7 @@ class LocalImagesImpl(context: Context) : LocalImageApi {
|
|||||||
private fun decodeSource(uri: Uri, target: Size, signal: CancellationSignal): Bitmap {
|
private fun decodeSource(uri: Uri, target: Size, signal: CancellationSignal): Bitmap {
|
||||||
signal.throwIfCanceled()
|
signal.throwIfCanceled()
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val source = ImageDecoder.createSource(resolver, uri)
|
ImageDecoder.createSource(resolver, uri).decodeBitmap(target)
|
||||||
signal.throwIfCanceled()
|
|
||||||
ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
|
|
||||||
if (target.width > 0 && target.height > 0) {
|
|
||||||
val sample = max(1, min(info.size.width / target.width, info.size.height / target.height))
|
|
||||||
decoder.setTargetSampleSize(sample)
|
|
||||||
}
|
|
||||||
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
|
|
||||||
decoder.setTargetColorSpace(ColorSpace.get(ColorSpace.Named.SRGB))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
val ref =
|
val ref =
|
||||||
Glide.with(ctx).asBitmap().priority(Priority.IMMEDIATE).load(uri).disallowHardwareConfig()
|
Glide.with(ctx).asBitmap().priority(Priority.IMMEDIATE).load(uri).disallowHardwareConfig()
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package app.alextran.immich.images
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.ColorSpace
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.CancellationSignal
|
||||||
|
import android.util.Size
|
||||||
|
import app.alextran.immich.core.SSLConfig
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.Cache
|
||||||
|
import okhttp3.ConnectionPool
|
||||||
|
import okhttp3.Dispatcher
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
data class RemoteRequest(
|
||||||
|
val callback: (Result<Map<String, Long>>) -> Unit,
|
||||||
|
val cancellationSignal: CancellationSignal,
|
||||||
|
)
|
||||||
|
|
||||||
|
class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
||||||
|
private val requestMap = ConcurrentHashMap<Long, RemoteRequest>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
System.loadLibrary("native_buffer")
|
||||||
|
cacheDir = context.cacheDir
|
||||||
|
client = buildClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
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
|
||||||
|
|
||||||
|
val CANCELLED = Result.success<Map<String, Long>>(emptyMap())
|
||||||
|
private val decodePool = Executors.newFixedThreadPool(
|
||||||
|
(Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(2)
|
||||||
|
)
|
||||||
|
|
||||||
|
private var cacheDir: File? = null
|
||||||
|
private var client: OkHttpClient? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
SSLConfig.addListener(::invalidateClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invalidateClient() {
|
||||||
|
client?.let {
|
||||||
|
it.dispatcher.cancelAll()
|
||||||
|
it.connectionPool.evictAll()
|
||||||
|
it.cache?.close()
|
||||||
|
}
|
||||||
|
client = buildClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildClient(): OkHttpClient {
|
||||||
|
val connectionPool = ConnectionPool(
|
||||||
|
maxIdleConnections = KEEP_ALIVE_CONNECTIONS,
|
||||||
|
keepAliveDuration = KEEP_ALIVE_DURATION_MINUTES,
|
||||||
|
timeUnit = TimeUnit.MINUTES
|
||||||
|
)
|
||||||
|
|
||||||
|
val builder = OkHttpClient.Builder()
|
||||||
|
.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))
|
||||||
|
}
|
||||||
|
|
||||||
|
val sslSocketFactory = SSLConfig.sslSocketFactory
|
||||||
|
val trustManager = SSLConfig.trustManager
|
||||||
|
if (sslSocketFactory != null && trustManager != null) {
|
||||||
|
builder.sslSocketFactory(sslSocketFactory, trustManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun requestImage(
|
||||||
|
url: String,
|
||||||
|
headers: Map<String, String>,
|
||||||
|
requestId: Long,
|
||||||
|
callback: (Result<Map<String, Long>>) -> Unit
|
||||||
|
) {
|
||||||
|
val client = client ?: return callback(Result.failure(RuntimeException("No client")))
|
||||||
|
val signal = CancellationSignal()
|
||||||
|
val requestBuilder = Request.Builder().url(url)
|
||||||
|
headers.forEach(requestBuilder::addHeader)
|
||||||
|
|
||||||
|
val call = client.newCall(requestBuilder.build())
|
||||||
|
signal.setOnCancelListener(call::cancel)
|
||||||
|
val request = RemoteRequest(callback, signal)
|
||||||
|
requestMap[requestId] = request
|
||||||
|
|
||||||
|
call.enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
requestMap.remove(requestId)
|
||||||
|
val result = if (signal.isCanceled) CANCELLED else Result.failure(e)
|
||||||
|
callback(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
decodePool.execute {
|
||||||
|
try {
|
||||||
|
signal.throwIfCanceled()
|
||||||
|
val bytes = response.takeIf { it.isSuccessful }?.body?.bytes()
|
||||||
|
?: return@execute callback(Result.failure(IOException(response.toString())))
|
||||||
|
signal.throwIfCanceled()
|
||||||
|
val bitmap = decodeImage(bytes)
|
||||||
|
signal.throwIfCanceled()
|
||||||
|
val res = bitmap.toNativeBuffer()
|
||||||
|
callback(Result.success(res))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val result = if (signal.isCanceled) CANCELLED else Result.failure(e)
|
||||||
|
callback(result)
|
||||||
|
} finally {
|
||||||
|
requestMap.remove(requestId)
|
||||||
|
response.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancelRequest(requestId: Long) {
|
||||||
|
val request = requestMap.remove(requestId) ?: return
|
||||||
|
request.cancellationSignal.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodeImage(bytes: ByteArray): Bitmap {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
ImageDecoder.createSource(ByteBuffer.wrap(bytes)).decodeBitmap()
|
||||||
|
} else {
|
||||||
|
val options = BitmapFactory.Options().apply {
|
||||||
|
inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||||
|
inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB)
|
||||||
|
}
|
||||||
|
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
|||||||
try! FileManager.default.createDirectory(at: thumbnailPath, withIntermediateDirectories: true)
|
try! FileManager.default.createDirectory(at: thumbnailPath, withIntermediateDirectories: true)
|
||||||
config.urlCache = URLCache(
|
config.urlCache = URLCache(
|
||||||
memoryCapacity: 0,
|
memoryCapacity: 0,
|
||||||
diskCapacity: 10 << 30,
|
diskCapacity: 1 << 30,
|
||||||
directory: thumbnailPath
|
directory: thumbnailPath
|
||||||
)
|
)
|
||||||
config.httpMaximumConnectionsPerHost = 16
|
config.httpMaximumConnectionsPerHost = 16
|
||||||
|
|||||||
Reference in New Issue
Block a user