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 {
def markwon_version = "4.6.2"
implementation project(':client')
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-splashscreen:1.0.1'
@@ -76,12 +77,12 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'com.github.cyb3rko:QuickPermissions-Kotlin:1.1.3'
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'io.noties.markwon:core:4.6.2'
implementation 'io.noties.markwon:image-picasso:4.6.2'
implementation 'io.noties.markwon:image:4.6.2'
implementation 'io.noties.markwon:ext-tables:4.6.2'
implementation 'io.noties.markwon:ext-strikethrough:4.6.2'
implementation 'io.coil-kt:coil:2.6.0'
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:image-coil:$markwon_version"
implementation "io.noties.markwon:image:$markwon_version"
implementation "io.noties.markwon:ext-tables:$markwon_version"
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
implementation 'org.tinylog:tinylog-api-kotlin: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.TypefaceSpan
import androidx.core.content.ContextCompat
import com.squareup.picasso.Picasso
import coil.ImageLoader
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
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.tables.TableAwareMovementMethod
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 org.commonmark.ext.gfm.tables.TableCell
import org.commonmark.ext.gfm.tables.TablesExtension
@@ -36,11 +36,11 @@ import org.commonmark.node.StrongEmphasis
import org.commonmark.parser.Parser
internal object MarkwonFactory {
fun createForMessage(context: Context, picasso: Picasso): Markwon {
fun createForMessage(context: Context, imageLoader: ImageLoader): Markwon {
return Markwon.builder(context)
.usePlugin(CorePlugin.create())
.usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create()))
.usePlugin(PicassoImagesPlugin.create(picasso))
.usePlugin(CoilImagesPlugin.create(context, imageLoader))
.usePlugin(StrikethroughPlugin.create())
.usePlugin(TablePlugin.create(context))
.usePlugin(object : AbstractMarkwonPlugin() {
@@ -52,13 +52,13 @@ internal object MarkwonFactory {
.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 bulletGapWidth = (8 * context.resources.displayMetrics.density + 0.5f).toInt()
return Markwon.builder(context)
.usePlugin(CorePlugin.create())
.usePlugin(PicassoImagesPlugin.create(picasso))
.usePlugin(CoilImagesPlugin.create(context, imageLoader))
.usePlugin(StrikethroughPlugin.create())
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {

View File

@@ -3,19 +3,15 @@ package com.github.gotify
import android.app.Activity
import android.app.ActivityManager
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.text.format.DateUtils
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import coil.target.Target
import com.github.gotify.client.JSON
import com.google.android.material.snackbar.Snackbar
import com.google.gson.Gson
import com.squareup.picasso.Picasso.LoadedFrom
import com.squareup.picasso.Target
import java.io.BufferedReader
import java.io.IOException
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 {
override fun onBitmapLoaded(bitmap: Bitmap, from: LoadedFrom) {
drawableReceiver.loaded(BitmapDrawable(resources, bitmap))
override fun onSuccess(result: Drawable) {
drawableReceiver.loaded(result)
}
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
Logger.error(e, "Bitmap failed")
override fun onError(error: Drawable?) {
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.RecyclerView
import androidx.viewbinding.ViewBinding
import coil.ImageLoader
import coil.load
import com.github.gotify.MarkwonFactory
import com.github.gotify.R
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.MessageItemCompactBinding
import com.github.gotify.messages.provider.MessageWithImage
import com.squareup.picasso.Picasso
import io.noties.markwon.Markwon
import java.text.DateFormat
import java.util.Date
@@ -35,11 +36,11 @@ import org.threeten.bp.OffsetDateTime
internal class ListMessageAdapter(
private val context: Context,
private val settings: Settings,
private val picasso: Picasso,
private val imageLoader: ImageLoader,
private val delete: Delete
) : ListAdapter<MessageWithImage, ListMessageAdapter.ViewHolder>(DiffCallback) {
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 =
context.resources.getString(R.string.time_format_value_relative)
@@ -81,10 +82,11 @@ internal class ListMessageAdapter(
}
holder.title.text = message.message.title
if (message.image != null) {
picasso.load(Utils.resolveAbsoluteUrl("${settings.url}/", message.image))
.error(R.drawable.ic_alarm)
.placeholder(R.drawable.ic_placeholder)
.into(holder.image)
val url = Utils.resolveAbsoluteUrl("${settings.url}/", message.image)
holder.image.load(url, imageLoader) {
error(R.drawable.ic_alarm)
placeholder(R.drawable.ic_placeholder)
}
}
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.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.request.ImageRequest
import com.github.gotify.BuildConfig
import com.github.gotify.MissedMessageUtil
import com.github.gotify.R
@@ -102,7 +103,7 @@ internal class MessagesActivity :
listMessageAdapter = ListMessageAdapter(
this,
viewModel.settings,
viewModel.picassoHandler.get()
viewModel.coilHandler.get()
) { message ->
scheduleDeletion(message)
}
@@ -170,9 +171,9 @@ internal class MessagesActivity :
private fun refreshAll() {
try {
viewModel.picassoHandler.evict()
viewModel.coilHandler.evict()
} catch (e: IOException) {
Logger.error(e, "Problem evicting Picasso cache")
Logger.error(e, "Problem evicting Coil cache")
}
startActivity(Intent(this, InitializationActivity::class.java))
finish()
@@ -201,15 +202,18 @@ internal class MessagesActivity :
val item = menu.add(R.id.apps, index, APPLICATION_ORDER, app.name)
item.isCheckable = true
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.picassoHandler
.get()
.load(Utils.resolveAbsoluteUrl(viewModel.settings.url + "/", app.image))
val request = ImageRequest.Builder(this)
.data(Utils.resolveAbsoluteUrl(viewModel.settings.url + "/", app.image))
.error(R.drawable.ic_alarm)
.placeholder(R.drawable.ic_placeholder)
.resize(100, 100)
.into(t)
.size(100, 100)
.target(t)
.build()
viewModel.coilHandler
.get()
.enqueue(request)
}
selectAppInMenu(selectedItem)
}

View File

@@ -2,18 +2,18 @@ package com.github.gotify.messages
import android.app.Activity
import androidx.lifecycle.ViewModel
import coil.target.Target
import com.github.gotify.Settings
import com.github.gotify.api.ClientFactory
import com.github.gotify.client.api.MessageApi
import com.github.gotify.messages.provider.ApplicationHolder
import com.github.gotify.messages.provider.MessageFacade
import com.github.gotify.messages.provider.MessageState
import com.github.gotify.picasso.PicassoHandler
import com.squareup.picasso.Target
import com.github.gotify.CoilHandler
internal class MessagesModel(parentView: Activity) : ViewModel() {
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 appsHolder = ApplicationHolder(parentView, client)
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.IntentUrlDialogActivity
import com.github.gotify.messages.MessagesActivity
import com.github.gotify.picasso.PicassoHandler
import com.github.gotify.CoilHandler
import io.noties.markwon.Markwon
import kotlinx.coroutines.runBlocking
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong
import org.tinylog.kotlin.Logger
@@ -62,7 +63,7 @@ internal class WebSocketService : Service() {
private val lastReceivedMessage = AtomicLong(NOT_LOADED)
private lateinit var missingMessageUtil: MissedMessageUtil
private lateinit var picassoHandler: PicassoHandler
private lateinit var coilHandler: CoilHandler
private lateinit var markwon: Markwon
override fun onCreate() {
@@ -75,8 +76,8 @@ internal class WebSocketService : Service() {
)
missingMessageUtil = MissedMessageUtil(client.createService(MessageApi::class.java))
Logger.info("Create ${javaClass.simpleName}")
picassoHandler = PicassoHandler(this, settings)
markwon = MarkwonFactory.createForNotification(this, picassoHandler.get())
coilHandler = CoilHandler(this, settings)
markwon = MarkwonFactory.createForNotification(this, coilHandler.get())
}
override fun onDestroy() {
@@ -377,11 +378,15 @@ internal class WebSocketService : Service() {
showNotificationGroup(channelId)
}
val largeIcon = runBlocking {
coilHandler.getIcon(appIdToApp[appId])
}
b.setAutoCancel(true)
.setDefaults(Notification.DEFAULT_ALL)
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_gotify)
.setLargeIcon(picassoHandler.getIcon(appIdToApp[appId]))
.setLargeIcon(largeIcon)
.setTicker("${getString(R.string.app_name)} - $title")
.setGroup(NotificationSupport.Group.MESSAGES)
.setContentTitle(title)
@@ -408,10 +413,12 @@ internal class WebSocketService : Service() {
if (notificationImageUrl != null) {
try {
b.setStyle(
NotificationCompat.BigPictureStyle()
.bigPicture(picassoHandler.getImageFromUrl(notificationImageUrl))
)
runBlocking {
b.setStyle(
NotificationCompat.BigPictureStyle()
.bigPicture(coilHandler.getImageFromUrl(notificationImageUrl))
)
}
} catch (e: Exception) {
Logger.error(e, "Error loading bigImageUrl")
}