Replace Picasso with Coil

This commit is contained in:
Niko Diamadis
2024-03-14 06:42:30 +01:00
parent 286074386d
commit 31649fa51b
9 changed files with 122 additions and 118 deletions

View File

@@ -66,6 +66,7 @@ if (project.hasProperty('sign')) {
} }
dependencies { dependencies {
def markwon_version = "4.6.2"
implementation project(':client') implementation project(':client')
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'androidx.core:core-splashscreen:1.0.1'
@@ -76,12 +77,12 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'com.github.cyb3rko:QuickPermissions-Kotlin:1.1.3' implementation 'com.github.cyb3rko:QuickPermissions-Kotlin:1.1.3'
implementation 'com.squareup.picasso:picasso:2.71828' implementation 'io.coil-kt:coil:2.6.0'
implementation 'io.noties.markwon:core:4.6.2' implementation "io.noties.markwon:core:$markwon_version"
implementation 'io.noties.markwon:image-picasso:4.6.2' implementation "io.noties.markwon:image-coil:$markwon_version"
implementation 'io.noties.markwon:image:4.6.2' implementation "io.noties.markwon:image:$markwon_version"
implementation 'io.noties.markwon:ext-tables:4.6.2' implementation "io.noties.markwon:ext-tables:$markwon_version"
implementation 'io.noties.markwon:ext-strikethrough:4.6.2' implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
implementation 'org.tinylog:tinylog-api-kotlin:2.6.2' implementation 'org.tinylog:tinylog-api-kotlin:2.6.2'
implementation 'org.tinylog:tinylog-impl:2.6.2' implementation 'org.tinylog:tinylog-impl:2.6.2'

View File

@@ -0,0 +1,62 @@
package com.github.gotify
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import coil.disk.DiskCache
import coil.request.ImageRequest
import com.github.gotify.api.CertUtils
import com.github.gotify.client.model.Application
import okhttp3.OkHttpClient
import org.tinylog.kotlin.Logger
import java.io.IOException
internal class CoilHandler(private val context: Context, private val settings: Settings) {
private val imageLoader = makeImageLoader()
private fun makeImageLoader(): ImageLoader {
val builder = OkHttpClient.Builder()
CertUtils.applySslSettings(builder, settings.sslSettings())
return ImageLoader.Builder(context)
.okHttpClient(builder.build())
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("coil-cache"))
.build()
}
.build()
}
@Throws(IOException::class)
suspend fun getImageFromUrl(url: String?): Bitmap {
val request = ImageRequest.Builder(context)
.data(url)
.build()
return (imageLoader.execute(request).drawable as BitmapDrawable).bitmap
}
suspend 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)
@Throws(IOException::class)
fun evict() {
imageLoader.diskCache?.directory?.toFile()?.deleteRecursively()
}
}

View File

@@ -10,7 +10,7 @@ import android.text.style.RelativeSizeSpan
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.text.style.TypefaceSpan import android.text.style.TypefaceSpan
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.squareup.picasso.Picasso import coil.ImageLoader
import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonSpansFactory import io.noties.markwon.MarkwonSpansFactory
@@ -22,7 +22,7 @@ import io.noties.markwon.core.MarkwonTheme
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import io.noties.markwon.ext.tables.TableAwareMovementMethod import io.noties.markwon.ext.tables.TableAwareMovementMethod
import io.noties.markwon.ext.tables.TablePlugin import io.noties.markwon.ext.tables.TablePlugin
import io.noties.markwon.image.picasso.PicassoImagesPlugin import io.noties.markwon.image.coil.CoilImagesPlugin
import io.noties.markwon.movement.MovementMethodPlugin import io.noties.markwon.movement.MovementMethodPlugin
import org.commonmark.ext.gfm.tables.TableCell import org.commonmark.ext.gfm.tables.TableCell
import org.commonmark.ext.gfm.tables.TablesExtension import org.commonmark.ext.gfm.tables.TablesExtension
@@ -36,11 +36,11 @@ import org.commonmark.node.StrongEmphasis
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
internal object MarkwonFactory { internal object MarkwonFactory {
fun createForMessage(context: Context, picasso: Picasso): Markwon { fun createForMessage(context: Context, imageLoader: ImageLoader): Markwon {
return Markwon.builder(context) return Markwon.builder(context)
.usePlugin(CorePlugin.create()) .usePlugin(CorePlugin.create())
.usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create())) .usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create()))
.usePlugin(PicassoImagesPlugin.create(picasso)) .usePlugin(CoilImagesPlugin.create(context, imageLoader))
.usePlugin(StrikethroughPlugin.create()) .usePlugin(StrikethroughPlugin.create())
.usePlugin(TablePlugin.create(context)) .usePlugin(TablePlugin.create(context))
.usePlugin(object : AbstractMarkwonPlugin() { .usePlugin(object : AbstractMarkwonPlugin() {
@@ -52,13 +52,13 @@ internal object MarkwonFactory {
.build() .build()
} }
fun createForNotification(context: Context, picasso: Picasso): Markwon { fun createForNotification(context: Context, imageLoader: ImageLoader): Markwon {
val headingSizes = floatArrayOf(2f, 1.5f, 1.17f, 1f, .83f, .67f) val headingSizes = floatArrayOf(2f, 1.5f, 1.17f, 1f, .83f, .67f)
val bulletGapWidth = (8 * context.resources.displayMetrics.density + 0.5f).toInt() val bulletGapWidth = (8 * context.resources.displayMetrics.density + 0.5f).toInt()
return Markwon.builder(context) return Markwon.builder(context)
.usePlugin(CorePlugin.create()) .usePlugin(CorePlugin.create())
.usePlugin(PicassoImagesPlugin.create(picasso)) .usePlugin(CoilImagesPlugin.create(context, imageLoader))
.usePlugin(StrikethroughPlugin.create()) .usePlugin(StrikethroughPlugin.create())
.usePlugin(object : AbstractMarkwonPlugin() { .usePlugin(object : AbstractMarkwonPlugin() {
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) { override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {

View File

@@ -3,19 +3,15 @@ package com.github.gotify
import android.app.Activity import android.app.Activity
import android.app.ActivityManager import android.app.ActivityManager
import android.content.Context import android.content.Context
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import coil.target.Target
import com.github.gotify.client.JSON import com.github.gotify.client.JSON
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.gson.Gson import com.google.gson.Gson
import com.squareup.picasso.Picasso.LoadedFrom
import com.squareup.picasso.Target
import java.io.BufferedReader import java.io.BufferedReader
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@@ -72,17 +68,15 @@ internal object Utils {
} }
} }
fun toDrawable(resources: Resources?, drawableReceiver: DrawableReceiver): Target { fun toDrawable(drawableReceiver: DrawableReceiver): Target {
return object : Target { return object : Target {
override fun onBitmapLoaded(bitmap: Bitmap, from: LoadedFrom) { override fun onSuccess(result: Drawable) {
drawableReceiver.loaded(BitmapDrawable(resources, bitmap)) drawableReceiver.loaded(result)
} }
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) { override fun onError(error: Drawable?) {
Logger.error(e, "Bitmap failed") Logger.error("Bitmap failed")
} }
override fun onPrepareLoad(placeHolderDrawable: Drawable) {}
} }
} }

View File

@@ -18,6 +18,8 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import coil.ImageLoader
import coil.load
import com.github.gotify.MarkwonFactory import com.github.gotify.MarkwonFactory
import com.github.gotify.R import com.github.gotify.R
import com.github.gotify.Settings import com.github.gotify.Settings
@@ -26,7 +28,6 @@ import com.github.gotify.client.model.Message
import com.github.gotify.databinding.MessageItemBinding import com.github.gotify.databinding.MessageItemBinding
import com.github.gotify.databinding.MessageItemCompactBinding import com.github.gotify.databinding.MessageItemCompactBinding
import com.github.gotify.messages.provider.MessageWithImage import com.github.gotify.messages.provider.MessageWithImage
import com.squareup.picasso.Picasso
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import java.text.DateFormat import java.text.DateFormat
import java.util.Date import java.util.Date
@@ -35,11 +36,11 @@ import org.threeten.bp.OffsetDateTime
internal class ListMessageAdapter( internal class ListMessageAdapter(
private val context: Context, private val context: Context,
private val settings: Settings, private val settings: Settings,
private val picasso: Picasso, private val imageLoader: ImageLoader,
private val delete: Delete private val delete: Delete
) : ListAdapter<MessageWithImage, ListMessageAdapter.ViewHolder>(DiffCallback) { ) : ListAdapter<MessageWithImage, ListMessageAdapter.ViewHolder>(DiffCallback) {
private val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) private val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val markwon: Markwon = MarkwonFactory.createForMessage(context, picasso) private val markwon: Markwon = MarkwonFactory.createForMessage(context, imageLoader)
private val timeFormatRelative = private val timeFormatRelative =
context.resources.getString(R.string.time_format_value_relative) context.resources.getString(R.string.time_format_value_relative)
@@ -81,10 +82,11 @@ internal class ListMessageAdapter(
} }
holder.title.text = message.message.title holder.title.text = message.message.title
if (message.image != null) { if (message.image != null) {
picasso.load(Utils.resolveAbsoluteUrl("${settings.url}/", message.image)) val url = Utils.resolveAbsoluteUrl("${settings.url}/", message.image)
.error(R.drawable.ic_alarm) holder.image.load(url, imageLoader) {
.placeholder(R.drawable.ic_placeholder) error(R.drawable.ic_alarm)
.into(holder.image) placeholder(R.drawable.ic_placeholder)
}
} }
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)

View File

@@ -29,6 +29,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.request.ImageRequest
import com.github.gotify.BuildConfig import com.github.gotify.BuildConfig
import com.github.gotify.MissedMessageUtil import com.github.gotify.MissedMessageUtil
import com.github.gotify.R import com.github.gotify.R
@@ -102,7 +103,7 @@ internal class MessagesActivity :
listMessageAdapter = ListMessageAdapter( listMessageAdapter = ListMessageAdapter(
this, this,
viewModel.settings, viewModel.settings,
viewModel.picassoHandler.get() viewModel.coilHandler.get()
) { message -> ) { message ->
scheduleDeletion(message) scheduleDeletion(message)
} }
@@ -170,9 +171,9 @@ internal class MessagesActivity :
private fun refreshAll() { private fun refreshAll() {
try { try {
viewModel.picassoHandler.evict() viewModel.coilHandler.evict()
} catch (e: IOException) { } catch (e: IOException) {
Logger.error(e, "Problem evicting Picasso cache") Logger.error(e, "Problem evicting Coil cache")
} }
startActivity(Intent(this, InitializationActivity::class.java)) startActivity(Intent(this, InitializationActivity::class.java))
finish() finish()
@@ -201,15 +202,18 @@ internal class MessagesActivity :
val item = menu.add(R.id.apps, index, APPLICATION_ORDER, app.name) val item = menu.add(R.id.apps, index, APPLICATION_ORDER, app.name)
item.isCheckable = true item.isCheckable = true
if (app.id == viewModel.appId) selectedItem = item if (app.id == viewModel.appId) selectedItem = item
val t = Utils.toDrawable(resources) { icon -> item.icon = icon } val t = Utils.toDrawable { icon -> item.icon = icon }
viewModel.targetReferences.add(t) viewModel.targetReferences.add(t)
viewModel.picassoHandler val request = ImageRequest.Builder(this)
.get() .data(Utils.resolveAbsoluteUrl(viewModel.settings.url + "/", app.image))
.load(Utils.resolveAbsoluteUrl(viewModel.settings.url + "/", app.image))
.error(R.drawable.ic_alarm) .error(R.drawable.ic_alarm)
.placeholder(R.drawable.ic_placeholder) .placeholder(R.drawable.ic_placeholder)
.resize(100, 100) .size(100, 100)
.into(t) .target(t)
.build()
viewModel.coilHandler
.get()
.enqueue(request)
} }
selectAppInMenu(selectedItem) selectAppInMenu(selectedItem)
} }

View File

@@ -2,18 +2,18 @@ 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 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
import com.github.gotify.messages.provider.ApplicationHolder import com.github.gotify.messages.provider.ApplicationHolder
import com.github.gotify.messages.provider.MessageFacade import com.github.gotify.messages.provider.MessageFacade
import com.github.gotify.messages.provider.MessageState import com.github.gotify.messages.provider.MessageState
import com.github.gotify.picasso.PicassoHandler import com.github.gotify.CoilHandler
import com.squareup.picasso.Target
internal class MessagesModel(parentView: Activity) : ViewModel() { internal class MessagesModel(parentView: Activity) : ViewModel() {
val settings = Settings(parentView) val settings = Settings(parentView)
val picassoHandler = PicassoHandler(parentView, settings) val coilHandler = CoilHandler(parentView, settings)
val client = ClientFactory.clientToken(settings.url, settings.sslSettings(), settings.token) val client = ClientFactory.clientToken(settings.url, settings.sslSettings(), settings.token)
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)

View File

@@ -1,66 +0,0 @@
package com.github.gotify.picasso
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.github.gotify.R
import com.github.gotify.Settings
import com.github.gotify.Utils
import com.github.gotify.api.CertUtils
import com.github.gotify.client.model.Application
import com.squareup.picasso.OkHttp3Downloader
import com.squareup.picasso.Picasso
import java.io.File
import java.io.IOException
import okhttp3.Cache
import okhttp3.OkHttpClient
import org.tinylog.kotlin.Logger
internal class PicassoHandler(private val context: Context, private val settings: Settings) {
companion object {
private const val PICASSO_CACHE_SIZE = 50 * 1024 * 1024 // 50 MB
private const val PICASSO_CACHE_SUBFOLDER = "picasso-cache"
}
private val picassoCache = Cache(
File(context.cacheDir, PICASSO_CACHE_SUBFOLDER),
PICASSO_CACHE_SIZE.toLong()
)
private val picasso = makePicasso()
private fun makePicasso(): Picasso {
val builder = OkHttpClient.Builder()
builder.cache(picassoCache)
CertUtils.applySslSettings(builder, settings.sslSettings())
val downloader = OkHttp3Downloader(builder.build())
return Picasso.Builder(context)
.addRequestHandler(PicassoDataRequestHandler())
.downloader(downloader)
.build()
}
@Throws(IOException::class)
fun getImageFromUrl(url: String?): Bitmap = picasso.load(url).get()
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() = picasso
@Throws(IOException::class)
fun evict() {
picassoCache.evictAll()
}
}

View File

@@ -34,8 +34,9 @@ import com.github.gotify.log.UncaughtExceptionHandler
import com.github.gotify.messages.Extras import com.github.gotify.messages.Extras
import com.github.gotify.messages.IntentUrlDialogActivity import com.github.gotify.messages.IntentUrlDialogActivity
import com.github.gotify.messages.MessagesActivity import com.github.gotify.messages.MessagesActivity
import com.github.gotify.picasso.PicassoHandler import com.github.gotify.CoilHandler
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import kotlinx.coroutines.runBlocking
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
import org.tinylog.kotlin.Logger import org.tinylog.kotlin.Logger
@@ -62,7 +63,7 @@ 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 picassoHandler: PicassoHandler private lateinit var coilHandler: CoilHandler
private lateinit var markwon: Markwon private lateinit var markwon: Markwon
override fun onCreate() { override fun onCreate() {
@@ -75,8 +76,8 @@ internal class WebSocketService : Service() {
) )
missingMessageUtil = MissedMessageUtil(client.createService(MessageApi::class.java)) missingMessageUtil = MissedMessageUtil(client.createService(MessageApi::class.java))
Logger.info("Create ${javaClass.simpleName}") Logger.info("Create ${javaClass.simpleName}")
picassoHandler = PicassoHandler(this, settings) coilHandler = CoilHandler(this, settings)
markwon = MarkwonFactory.createForNotification(this, picassoHandler.get()) markwon = MarkwonFactory.createForNotification(this, coilHandler.get())
} }
override fun onDestroy() { override fun onDestroy() {
@@ -377,11 +378,15 @@ internal class WebSocketService : Service() {
showNotificationGroup(channelId) showNotificationGroup(channelId)
} }
val largeIcon = runBlocking {
coilHandler.getIcon(appIdToApp[appId])
}
b.setAutoCancel(true) b.setAutoCancel(true)
.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(picassoHandler.getIcon(appIdToApp[appId])) .setLargeIcon(largeIcon)
.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)
@@ -408,10 +413,12 @@ internal class WebSocketService : Service() {
if (notificationImageUrl != null) { if (notificationImageUrl != null) {
try { try {
b.setStyle( runBlocking {
NotificationCompat.BigPictureStyle() b.setStyle(
.bigPicture(picassoHandler.getImageFromUrl(notificationImageUrl)) NotificationCompat.BigPictureStyle()
) .bigPicture(coilHandler.getImageFromUrl(notificationImageUrl))
)
}
} catch (e: Exception) { } catch (e: Exception) {
Logger.error(e, "Error loading bigImageUrl") Logger.error(e, "Error loading bigImageUrl")
} }