Merge branch 'master' into client-certificate-auth

This commit is contained in:
Niko Diamadis
2024-04-23 23:19:04 +02:00
13 changed files with 146 additions and 177 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,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")
}
}
}

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.net.MalformedURLException import java.net.MalformedURLException
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
@@ -67,17 +63,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

@@ -76,17 +76,14 @@ internal object CertUtils {
if (customManagers || !settings.validateSSL) { if (customManagers || !settings.validateSSL) {
val context = SSLContext.getInstance("TLS") val context = SSLContext.getInstance("TLS")
context.init(keyManagers, trustManagers, SecureRandom()) context.init(keyManagers, trustManagers, SecureRandom())
if (trustManagers != null) { if (trustManagers == null) {
// Use custom trust manager
builder.sslSocketFactory(
context.socketFactory,
trustManagers[0] as X509TrustManager
)
} else {
// Fall back to system trust managers // Fall back to system trust managers
@Suppress("DEPRECATION") trustManagers = defaultSystemTrustManager()
builder.sslSocketFactory(context.socketFactory)
} }
builder.sslSocketFactory(
context.socketFactory,
trustManagers[0] as X509TrustManager
)
} }
} catch (e: Exception) { } catch (e: Exception) {
// We shouldn't have issues since the cert is verified on login. // We shouldn't have issues since the cert is verified on login.
@@ -124,4 +121,12 @@ internal object CertUtils {
keyManagerFactory.init(keyStore, certPassword.toCharArray()) keyManagerFactory.init(keyStore, certPassword.toCharArray())
return keyManagerFactory.keyManagers return keyManagerFactory.keyManagers
} }
private fun defaultSystemTrustManager(): Array<TrustManager> {
val trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()
)
trustManagerFactory.init(null as KeyStore?)
return trustManagerFactory.trustManagers
}
} }

View File

@@ -38,7 +38,7 @@ import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.InputStream import java.io.InputStream
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() {
@@ -130,13 +130,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()
} }

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
@@ -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) {

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.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) 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)

View File

@@ -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)
}
}

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

@@ -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
} }

View File

@@ -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() {
@@ -71,8 +71,8 @@ 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}")
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,7 +377,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)
@@ -406,7 +406,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")