Merge pull request #337 from cyb3rko/picasso-to-coil
Replace Picasso with Coil
This commit is contained in:
@@ -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'
|
||||||
|
|||||||
67
app/src/main/kotlin/com/github/gotify/CoilHandler.kt
Normal file
67
app/src/main/kotlin/com/github/gotify/CoilHandler.kt
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
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.executeBlocking
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.github.gotify.api.CertUtils
|
||||||
|
import com.github.gotify.client.model.Application
|
||||||
|
import java.io.IOException
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import org.tinylog.kotlin.Logger
|
||||||
|
|
||||||
|
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)
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import com.github.gotify.log.UncaughtExceptionHandler
|
|||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import org.tinylog.kotlin.Logger
|
import org.tinylog.kotlin.Logger
|
||||||
|
|
||||||
internal class LoginActivity : AppCompatActivity() {
|
internal class LoginActivity : AppCompatActivity() {
|
||||||
@@ -101,13 +101,13 @@ internal class LoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun doCheckUrl() {
|
private fun doCheckUrl() {
|
||||||
val url = binding.gotifyUrlEditext.text.toString().trim().trimEnd('/')
|
val url = binding.gotifyUrlEditext.text.toString().trim().trimEnd('/')
|
||||||
val parsedUrl = HttpUrl.parse(url)
|
val parsedUrl = url.toHttpUrlOrNull()
|
||||||
if (parsedUrl == null) {
|
if (parsedUrl == null) {
|
||||||
Utils.showSnackBar(this, "Invalid URL (include http:// or https://)")
|
Utils.showSnackBar(this, "Invalid URL (include http:// or https://)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("http" == parsedUrl.scheme()) {
|
if ("http" == parsedUrl.scheme) {
|
||||||
showHttpWarning()
|
showHttpWarning()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -57,7 +58,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
import com.google.android.material.snackbar.BaseTransientBottomBar.BaseCallback
|
import com.google.android.material.snackbar.BaseTransientBottomBar.BaseCallback
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import java.io.IOException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.tinylog.kotlin.Logger
|
import org.tinylog.kotlin.Logger
|
||||||
@@ -102,7 +102,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)
|
||||||
}
|
}
|
||||||
@@ -169,19 +169,20 @@ internal class MessagesActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshAll() {
|
private fun refreshAll() {
|
||||||
try {
|
viewModel.coilHandler.evict()
|
||||||
viewModel.picassoHandler.evict()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Logger.error(e, "Problem evicting Picasso cache")
|
|
||||||
}
|
|
||||||
startActivity(Intent(this, InitializationActivity::class.java))
|
startActivity(Intent(this, InitializationActivity::class.java))
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRefresh() {
|
private fun onRefresh() {
|
||||||
|
viewModel.coilHandler.evict()
|
||||||
viewModel.messages.clear()
|
viewModel.messages.clear()
|
||||||
launchCoroutine {
|
launchCoroutine {
|
||||||
loadMore(viewModel.appId)
|
loadMore(viewModel.appId).forEachIndexed { index, message ->
|
||||||
|
if (message.image != null) {
|
||||||
|
listMessageAdapter.notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -552,11 +556,12 @@ internal class MessagesActivity :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadMore(appId: Long) {
|
private suspend fun loadMore(appId: Long): List<MessageWithImage> {
|
||||||
val messagesWithImages = viewModel.messages.loadMore(appId)
|
val messagesWithImages = viewModel.messages.loadMore(appId)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateMessagesAndStopLoading(messagesWithImages)
|
updateMessagesAndStopLoading(messagesWithImages)
|
||||||
}
|
}
|
||||||
|
return messagesWithImages
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateMessagesForApplication(withLoadingSpinner: Boolean, appId: Long) {
|
private suspend fun updateMessagesForApplication(withLoadingSpinner: Boolean, appId: Long) {
|
||||||
|
|||||||
@@ -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.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
|
||||||
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.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)
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package com.github.gotify.picasso
|
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.util.Base64
|
|
||||||
import com.squareup.picasso.Picasso
|
|
||||||
import com.squareup.picasso.Request
|
|
||||||
import com.squareup.picasso.RequestHandler
|
|
||||||
import org.tinylog.kotlin.Logger
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapted from https://github.com/square/picasso/issues/1395#issuecomment-220929377 By
|
|
||||||
* https://github.com/SmartDengg
|
|
||||||
*/
|
|
||||||
internal class PicassoDataRequestHandler : RequestHandler() {
|
|
||||||
companion object {
|
|
||||||
private const val DATA_SCHEME = "data"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canHandleRequest(data: Request): Boolean {
|
|
||||||
val scheme = data.uri.scheme
|
|
||||||
return DATA_SCHEME.equals(scheme, ignoreCase = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun load(request: Request, networkPolicy: Int): Result {
|
|
||||||
val uri = request.uri.toString()
|
|
||||||
val imageDataBytes = uri.substring(uri.indexOf(",") + 1)
|
|
||||||
val bytes = Base64.decode(imageDataBytes.toByteArray(), Base64.DEFAULT)
|
|
||||||
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
|
||||||
|
|
||||||
if (bitmap == null) {
|
|
||||||
val show = if (uri.length > 50) uri.take(50) + "..." else uri
|
|
||||||
val malformed = RuntimeException("Malformed data uri: $show")
|
|
||||||
Logger.error(malformed, "Could not load image")
|
|
||||||
throw malformed
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result(bitmap, Picasso.LoadedFrom.NETWORK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@ import com.github.gotify.client.model.Message
|
|||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -89,7 +89,7 @@ internal class WebSocketConnection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun request(): Request {
|
private fun request(): Request {
|
||||||
val url = HttpUrl.parse(baseUrl)!!
|
val url = baseUrl.toHttpUrlOrNull()!!
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.addPathSegment("stream")
|
.addPathSegment("stream")
|
||||||
.addQueryParameter("token", token)
|
.addQueryParameter("token", token)
|
||||||
@@ -187,12 +187,12 @@ internal class WebSocketConnection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||||
val code = if (response != null) "StatusCode: ${response.code()}" else ""
|
val code = if (response != null) "StatusCode: ${response.code}" else ""
|
||||||
val message = if (response != null) response.message() else ""
|
val message = response?.message ?: ""
|
||||||
Logger.error(t) { "WebSocket($id): failure $code Message: $message" }
|
Logger.error(t) { "WebSocket($id): failure $code Message: $message" }
|
||||||
syncExec(id) {
|
syncExec(id) {
|
||||||
closed()
|
closed()
|
||||||
if (response != null && response.code() >= 400 && response.code() <= 499) {
|
if (response != null && response.code >= 400 && response.code <= 499) {
|
||||||
onBadRequest.execute(message)
|
onBadRequest.execute(message)
|
||||||
return@syncExec
|
return@syncExec
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +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.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
|
||||||
@@ -34,7 +35,6 @@ 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 io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
@@ -62,7 +62,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 +75,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() {
|
||||||
@@ -381,7 +381,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(picassoHandler.getIcon(appIdToApp[appId]))
|
.setLargeIcon(coilHandler.getIcon(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)
|
||||||
@@ -410,7 +410,7 @@ internal class WebSocketService : Service() {
|
|||||||
try {
|
try {
|
||||||
b.setStyle(
|
b.setStyle(
|
||||||
NotificationCompat.BigPictureStyle()
|
NotificationCompat.BigPictureStyle()
|
||||||
.bigPicture(picassoHandler.getImageFromUrl(notificationImageUrl))
|
.bigPicture(coilHandler.getImageFromUrl(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