fix: image loading when using markdown img and bigImageUrl
When receiving a message with the same image in the markdown body and in
the extras client::notification.bigImageUrl, then there is a clash on
the file system.
One request succeeds and the other fails with the following error. This
commit ensures that there is only one coil image loader instance, so
that there shouldn't be file system race conditions.
WebSocket(1): received message {"id":845,"appid":21,...}
Failed - http://192.168.178.2:8000/1.jpg?v=1718369188 - java.lang.IllegalStateException: closed
java.lang.IllegalStateException: closed
at okio.RealBufferedSource.rangeEquals(RealBufferedSource.kt:466)
at okio.RealBufferedSource.rangeEquals(RealBufferedSource.kt:130)
at coil.decode.SvgDecodeUtils.isSvg(DecodeUtils.kt:19)
at coil.decode.SvgDecoder$Factory.isApplicable(SvgDecoder.kt:104)
at coil.decode.SvgDecoder$Factory.create(SvgDecoder.kt:99)
at coil.ComponentRegistry.newDecoder(ComponentRegistry.kt:100)
at coil.intercept.EngineInterceptor.decode(EngineInterceptor.kt:197)
at coil.intercept.EngineInterceptor.access$decode(EngineInterceptor.kt:42)
at coil.intercept.EngineInterceptor$execute$executeResult$1.invokeSuspend(EngineInterceptor.kt:131)
at coil.intercept.EngineInterceptor$execute$executeResult$1.invoke(EngineInterceptor.kt)
at coil.intercept.EngineInterceptor$execute$executeResult$1.invoke(EngineInterceptor.kt)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:78)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:167)
at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
at coil.intercept.EngineInterceptor.execute(EngineInterceptor.kt:130)
at coil.intercept.EngineInterceptor.access$execute(EngineInterceptor.kt:42)
at coil.intercept.EngineInterceptor$execute$1.invokeSuspend(EngineInterceptor.kt)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
Successful (NETWORK) - http://192.168.178.2:8000/1.jpg?v=1718369188
This commit is contained in:
@@ -16,13 +16,62 @@ import java.io.IOException
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.tinylog.kotlin.Logger
|
import org.tinylog.kotlin.Logger
|
||||||
|
|
||||||
internal class CoilHandler(private val context: Context, private val settings: Settings) {
|
object CoilInstance {
|
||||||
private val imageLoader = makeImageLoader()
|
private var holder: Pair<SSLSettings, ImageLoader>? = null
|
||||||
|
|
||||||
private fun makeImageLoader(): ImageLoader {
|
@Throws(IOException::class)
|
||||||
|
fun getImageFromUrl(context: Context, url: String?): Bitmap {
|
||||||
|
val request = ImageRequest.Builder(context)
|
||||||
|
.data(url)
|
||||||
|
.build()
|
||||||
|
return (get(context).executeBlocking(request).drawable as BitmapDrawable).bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getIcon(context: Context, app: Application?): Bitmap {
|
||||||
|
if (app == null) {
|
||||||
|
return BitmapFactory.decodeResource(context.resources, R.drawable.gotify)
|
||||||
|
}
|
||||||
|
val baseUrl = Settings(context).url
|
||||||
|
try {
|
||||||
|
return getImageFromUrl(
|
||||||
|
context,
|
||||||
|
Utils.resolveAbsoluteUrl("$baseUrl/", app.image)
|
||||||
|
)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Logger.error(e, "Could not load image for notification")
|
||||||
|
}
|
||||||
|
return BitmapFactory.decodeResource(context.resources, R.drawable.gotify)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoilApi::class)
|
||||||
|
fun evict(context: Context) {
|
||||||
|
try {
|
||||||
|
get(context).apply {
|
||||||
|
diskCache?.clear()
|
||||||
|
memoryCache?.clear()
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Logger.error(e, "Problem evicting Coil cache")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun get(context: Context): ImageLoader {
|
||||||
|
val newSettings = Settings(context).sslSettings()
|
||||||
|
val copy = holder
|
||||||
|
if (copy != null && copy.first == newSettings) {
|
||||||
|
return copy.second
|
||||||
|
}
|
||||||
|
return makeImageLoader(context, newSettings).also { holder = it }.second
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeImageLoader(
|
||||||
|
context: Context,
|
||||||
|
sslSettings: SSLSettings
|
||||||
|
): Pair<SSLSettings, ImageLoader> {
|
||||||
val builder = OkHttpClient.Builder()
|
val builder = OkHttpClient.Builder()
|
||||||
CertUtils.applySslSettings(builder, settings.sslSettings())
|
CertUtils.applySslSettings(builder, sslSettings)
|
||||||
return ImageLoader.Builder(context)
|
val loader = ImageLoader.Builder(context)
|
||||||
.okHttpClient(builder.build())
|
.okHttpClient(builder.build())
|
||||||
.diskCache {
|
.diskCache {
|
||||||
DiskCache.Builder()
|
DiskCache.Builder()
|
||||||
@@ -33,39 +82,6 @@ internal class CoilHandler(private val context: Context, private val settings: S
|
|||||||
add(SvgDecoder.Factory())
|
add(SvgDecoder.Factory())
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
}
|
return sslSettings to loader
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getImageFromUrl(url: String?): Bitmap {
|
|
||||||
val request = ImageRequest.Builder(context)
|
|
||||||
.data(url)
|
|
||||||
.build()
|
|
||||||
return (imageLoader.executeBlocking(request).drawable as BitmapDrawable).bitmap
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIcon(app: Application?): Bitmap {
|
|
||||||
if (app == null) {
|
|
||||||
return BitmapFactory.decodeResource(context.resources, R.drawable.gotify)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return getImageFromUrl(
|
|
||||||
Utils.resolveAbsoluteUrl("${settings.url}/", app.image)
|
|
||||||
)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Logger.error(e, "Could not load image for notification")
|
|
||||||
}
|
|
||||||
return BitmapFactory.decodeResource(context.resources, R.drawable.gotify)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get() = imageLoader
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoilApi::class)
|
|
||||||
fun evict() {
|
|
||||||
try {
|
|
||||||
imageLoader.diskCache?.clear()
|
|
||||||
imageLoader.memoryCache?.clear()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Logger.error(e, "Problem evicting Coil cache")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.github.gotify
|
package com.github.gotify
|
||||||
|
|
||||||
internal class SSLSettings(
|
internal data class SSLSettings(
|
||||||
val validateSSL: Boolean,
|
val validateSSL: Boolean,
|
||||||
val caCertPath: String?,
|
val caCertPath: String?,
|
||||||
val clientCertPath: String?,
|
val clientCertPath: String?,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.github.gotify.BuildConfig
|
import com.github.gotify.BuildConfig
|
||||||
|
import com.github.gotify.CoilInstance
|
||||||
import com.github.gotify.MissedMessageUtil
|
import com.github.gotify.MissedMessageUtil
|
||||||
import com.github.gotify.R
|
import com.github.gotify.R
|
||||||
import com.github.gotify.Utils
|
import com.github.gotify.Utils
|
||||||
@@ -102,7 +103,7 @@ internal class MessagesActivity :
|
|||||||
listMessageAdapter = ListMessageAdapter(
|
listMessageAdapter = ListMessageAdapter(
|
||||||
this,
|
this,
|
||||||
viewModel.settings,
|
viewModel.settings,
|
||||||
viewModel.coilHandler.get()
|
CoilInstance.get(this)
|
||||||
) { message ->
|
) { message ->
|
||||||
scheduleDeletion(message)
|
scheduleDeletion(message)
|
||||||
}
|
}
|
||||||
@@ -169,13 +170,13 @@ internal class MessagesActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshAll() {
|
private fun refreshAll() {
|
||||||
viewModel.coilHandler.evict()
|
CoilInstance.evict(this)
|
||||||
startActivity(Intent(this, InitializationActivity::class.java))
|
startActivity(Intent(this, InitializationActivity::class.java))
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRefresh() {
|
private fun onRefresh() {
|
||||||
viewModel.coilHandler.evict()
|
CoilInstance.evict(this)
|
||||||
viewModel.messages.clear()
|
viewModel.messages.clear()
|
||||||
launchCoroutine {
|
launchCoroutine {
|
||||||
loadMore(viewModel.appId).forEachIndexed { index, message ->
|
loadMore(viewModel.appId).forEachIndexed { index, message ->
|
||||||
@@ -211,9 +212,7 @@ internal class MessagesActivity :
|
|||||||
.size(100, 100)
|
.size(100, 100)
|
||||||
.target(t)
|
.target(t)
|
||||||
.build()
|
.build()
|
||||||
viewModel.coilHandler
|
CoilInstance.get(this).enqueue(request)
|
||||||
.get()
|
|
||||||
.enqueue(request)
|
|
||||||
}
|
}
|
||||||
selectAppInMenu(selectedItem)
|
selectAppInMenu(selectedItem)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.github.gotify.messages
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import coil.target.Target
|
import coil.target.Target
|
||||||
import com.github.gotify.CoilHandler
|
|
||||||
import com.github.gotify.Settings
|
import com.github.gotify.Settings
|
||||||
import com.github.gotify.api.ClientFactory
|
import com.github.gotify.api.ClientFactory
|
||||||
import com.github.gotify.client.api.MessageApi
|
import com.github.gotify.client.api.MessageApi
|
||||||
@@ -13,7 +12,6 @@ import com.github.gotify.messages.provider.MessageState
|
|||||||
|
|
||||||
internal class MessagesModel(parentView: Activity) : ViewModel() {
|
internal class MessagesModel(parentView: Activity) : ViewModel() {
|
||||||
val settings = Settings(parentView)
|
val settings = Settings(parentView)
|
||||||
val coilHandler = CoilHandler(parentView, settings)
|
|
||||||
val client = ClientFactory.clientToken(settings)
|
val client = ClientFactory.clientToken(settings)
|
||||||
val appsHolder = ApplicationHolder(parentView, client)
|
val appsHolder = ApplicationHolder(parentView, client)
|
||||||
val messages = MessageFacade(client.createService(MessageApi::class.java), appsHolder)
|
val messages = MessageFacade(client.createService(MessageApi::class.java), appsHolder)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.github.gotify.BuildConfig
|
import com.github.gotify.BuildConfig
|
||||||
import com.github.gotify.CoilHandler
|
import com.github.gotify.CoilInstance
|
||||||
import com.github.gotify.MarkwonFactory
|
import com.github.gotify.MarkwonFactory
|
||||||
import com.github.gotify.MissedMessageUtil
|
import com.github.gotify.MissedMessageUtil
|
||||||
import com.github.gotify.NotificationSupport
|
import com.github.gotify.NotificationSupport
|
||||||
@@ -62,7 +62,6 @@ internal class WebSocketService : Service() {
|
|||||||
private val lastReceivedMessage = AtomicLong(NOT_LOADED)
|
private val lastReceivedMessage = AtomicLong(NOT_LOADED)
|
||||||
private lateinit var missingMessageUtil: MissedMessageUtil
|
private lateinit var missingMessageUtil: MissedMessageUtil
|
||||||
|
|
||||||
private lateinit var coilHandler: CoilHandler
|
|
||||||
private lateinit var markwon: Markwon
|
private lateinit var markwon: Markwon
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -71,8 +70,7 @@ internal class WebSocketService : Service() {
|
|||||||
val client = ClientFactory.clientToken(settings)
|
val client = ClientFactory.clientToken(settings)
|
||||||
missingMessageUtil = MissedMessageUtil(client.createService(MessageApi::class.java))
|
missingMessageUtil = MissedMessageUtil(client.createService(MessageApi::class.java))
|
||||||
Logger.info("Create ${javaClass.simpleName}")
|
Logger.info("Create ${javaClass.simpleName}")
|
||||||
coilHandler = CoilHandler(this, settings)
|
markwon = MarkwonFactory.createForNotification(this, CoilInstance.get(this))
|
||||||
markwon = MarkwonFactory.createForNotification(this, coilHandler.get())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@@ -377,7 +375,7 @@ internal class WebSocketService : Service() {
|
|||||||
.setDefaults(Notification.DEFAULT_ALL)
|
.setDefaults(Notification.DEFAULT_ALL)
|
||||||
.setWhen(System.currentTimeMillis())
|
.setWhen(System.currentTimeMillis())
|
||||||
.setSmallIcon(R.drawable.ic_gotify)
|
.setSmallIcon(R.drawable.ic_gotify)
|
||||||
.setLargeIcon(coilHandler.getIcon(appIdToApp[appId]))
|
.setLargeIcon(CoilInstance.getIcon(this, appIdToApp[appId]))
|
||||||
.setTicker("${getString(R.string.app_name)} - $title")
|
.setTicker("${getString(R.string.app_name)} - $title")
|
||||||
.setGroup(NotificationSupport.Group.MESSAGES)
|
.setGroup(NotificationSupport.Group.MESSAGES)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
@@ -406,7 +404,7 @@ internal class WebSocketService : Service() {
|
|||||||
try {
|
try {
|
||||||
b.setStyle(
|
b.setStyle(
|
||||||
NotificationCompat.BigPictureStyle()
|
NotificationCompat.BigPictureStyle()
|
||||||
.bigPicture(coilHandler.getImageFromUrl(notificationImageUrl))
|
.bigPicture(CoilInstance.getImageFromUrl(this, notificationImageUrl))
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.error(e, "Error loading bigImageUrl")
|
Logger.error(e, "Error loading bigImageUrl")
|
||||||
|
|||||||
Reference in New Issue
Block a user