Simplify and optimize Kotlin code snippets

This commit is contained in:
Niko Diamadis
2022-12-30 18:23:50 +01:00
parent c32d6a81bd
commit 188ef24e69
25 changed files with 212 additions and 264 deletions

View File

@@ -28,8 +28,7 @@ internal object MarkwonFactory {
.usePlugin(PicassoImagesPlugin.create(picasso)) .usePlugin(PicassoImagesPlugin.create(picasso))
.usePlugin(StrikethroughPlugin.create()) .usePlugin(StrikethroughPlugin.create())
.usePlugin(TablePlugin.create(context)) .usePlugin(TablePlugin.create(context))
.usePlugin( .usePlugin(object : AbstractMarkwonPlugin() {
object : AbstractMarkwonPlugin() {
override fun configureTheme(builder: MarkwonTheme.Builder) { override fun configureTheme(builder: MarkwonTheme.Builder) {
builder.linkColor(ContextCompat.getColor(context, R.color.hyperLink)) builder.linkColor(ContextCompat.getColor(context, R.color.hyperLink))
.isLinkUnderlined(true) .isLinkUnderlined(true)
@@ -39,17 +38,14 @@ internal object MarkwonFactory {
} }
fun createForNotification(context: Context, picasso: Picasso): Markwon { fun createForNotification(context: Context, picasso: Picasso): Markwon {
val headingSizes = floatArrayOf( val headingSizes = floatArrayOf(2f, 1.5f, 1.17f, 1f, .83f, .67f)
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(PicassoImagesPlugin.create(picasso))
.usePlugin(StrikethroughPlugin.create()) .usePlugin(StrikethroughPlugin.create())
.usePlugin( .usePlugin(object : AbstractMarkwonPlugin() {
object : AbstractMarkwonPlugin() {
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) { override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
builder.setFactory(Heading::class.java) { _, props: RenderProps? -> builder.setFactory(Heading::class.java) { _, props: RenderProps? ->
arrayOf<Any>( arrayOf<Any>(

View File

@@ -32,9 +32,9 @@ internal class MissedMessageUtil(private val api: MessageApi) {
val messages = pagedMessages!!.messages val messages = pagedMessages!!.messages
val filtered = filter(messages, till) val filtered = filter(messages, till)
result.addAll(filtered) result.addAll(filtered)
if (messages.size != filtered.size if (messages.size != filtered.size ||
|| messages.size == 0 messages.size == 0 ||
|| pagedMessages.paging.next == null) { pagedMessages.paging.next == null) {
break break
} }
since = pagedMessages.paging.since since = pagedMessages.paging.since

View File

@@ -1,3 +1,3 @@
package com.github.gotify package com.github.gotify
internal class SSLSettings(val validateSSL: Boolean, val cert: String) internal class SSLSettings(val validateSSL: Boolean, val cert: String?)

View File

@@ -9,8 +9,8 @@ internal class Settings(context: Context) {
var url: String var url: String
get() = sharedPreferences.getString("url", "")!! get() = sharedPreferences.getString("url", "")!!
set(value) = sharedPreferences.edit().putString("url", value).apply() set(value) = sharedPreferences.edit().putString("url", value).apply()
var token: String var token: String?
get() = sharedPreferences.getString("token", "")!! get() = sharedPreferences.getString("token", null)
set(value) = sharedPreferences.edit().putString("token", value).apply() set(value) = sharedPreferences.edit().putString("token", value).apply()
var user: User? = null var user: User? = null
get() { get() {
@@ -26,8 +26,8 @@ internal class Settings(context: Context) {
var serverVersion: String var serverVersion: String
get() = sharedPreferences.getString("version", "UNKNOWN")!! get() = sharedPreferences.getString("version", "UNKNOWN")!!
set(value) = sharedPreferences.edit().putString("version", value).apply() set(value) = sharedPreferences.edit().putString("version", value).apply()
var cert: String var cert: String?
get() = sharedPreferences.getString("cert", "")!! get() = sharedPreferences.getString("cert", null)
set(value) = sharedPreferences.edit().putString("cert", value).apply() set(value) = sharedPreferences.edit().putString("cert", value).apply()
var validateSSL: Boolean var validateSSL: Boolean
get() = sharedPreferences.getBoolean("validateSSL", true) get() = sharedPreferences.getBoolean("validateSSL", true)
@@ -37,13 +37,13 @@ internal class Settings(context: Context) {
sharedPreferences = context.getSharedPreferences("gotify", Context.MODE_PRIVATE) sharedPreferences = context.getSharedPreferences("gotify", Context.MODE_PRIVATE)
} }
fun tokenExists(): Boolean = token.isNotEmpty() fun tokenExists(): Boolean = !token.isNullOrEmpty()
fun clear() { fun clear() {
url = "" url = ""
token = "" token = null
validateSSL = true validateSSL = true
cert = "" cert = null
} }
fun setUser(name: String?, admin: Boolean) { fun setUser(name: String?, admin: Boolean) {

View File

@@ -49,7 +49,7 @@ internal object Utils {
.toString() .toString()
} }
fun resolveAbsoluteUrl(baseURL: String?, target: String?): String? { fun resolveAbsoluteUrl(baseURL: String, target: String?): String? {
return if (target == null) { return if (target == null) {
null null
} else try { } else try {

View File

@@ -1,7 +1,6 @@
package com.github.gotify.api package com.github.gotify.api
import java.io.IOException import java.io.IOException
import java.util.*
import retrofit2.Response import retrofit2.Response
internal class ApiException : Exception { internal class ApiException : Exception {
@@ -23,12 +22,5 @@ internal class ApiException : Exception {
code = 0 code = 0
} }
override fun toString(): String { override fun toString() = "Code($code) Response: ${body.take(200)}"
return String.format(
Locale.ENGLISH,
"Code(%d) Response: %s",
code,
body.substring(0, body.length.coerceAtMost(200))
)
}
} }

View File

@@ -17,13 +17,7 @@ internal class Callback<T> private constructor(
fun onError(t: ApiException) fun onError(t: ApiException)
} }
private class RetrofitCallback<T>(callback: Callback<T>) : retrofit2.Callback<T> { private class RetrofitCallback<T>(private val callback: Callback<T>) : retrofit2.Callback<T> {
private val callback: Callback<T>
init {
this.callback = callback
}
override fun onResponse(call: Call<T>, response: Response<T>) { override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) { if (response.isSuccessful) {
callback.onSuccess.onSuccess(response.body()) callback.onSuccess.onSuccess(response.body())

View File

@@ -16,14 +16,14 @@ import okhttp3.OkHttpClient
internal object CertUtils { internal object CertUtils {
@SuppressLint("CustomX509TrustManager") @SuppressLint("CustomX509TrustManager")
private val trustAll: X509TrustManager = object : X509TrustManager { private val trustAll = object : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager") @SuppressLint("TrustAllX509TrustManager")
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {} override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
@SuppressLint("TrustAllX509TrustManager") @SuppressLint("TrustAllX509TrustManager")
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {} override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf() override fun getAcceptedIssuers() = arrayOf<X509Certificate>()
} }
fun parseCertificate(cert: String): Certificate { fun parseCertificate(cert: String): Certificate {
@@ -45,8 +45,9 @@ internal object CertUtils {
builder.hostnameVerifier { _, _ -> true } builder.hostnameVerifier { _, _ -> true }
return return
} }
if (settings.cert != null) { val cert = settings.cert
val trustManagers = certToTrustManager(settings.cert) if (cert != null) {
val trustManagers = certToTrustManager(cert)
if (trustManagers.isNotEmpty()) { if (trustManagers.isNotEmpty()) {
val context = SSLContext.getInstance("TLS") val context = SSLContext.getInstance("TLS")
context.init(arrayOf(), trustManagers, SecureRandom()) context.init(arrayOf(), trustManagers, SecureRandom())
@@ -66,7 +67,7 @@ internal object CertUtils {
private fun certToTrustManager(cert: String): Array<TrustManager> { private fun certToTrustManager(cert: String): Array<TrustManager> {
val certificateFactory = CertificateFactory.getInstance("X.509") val certificateFactory = CertificateFactory.getInstance("X.509")
val certificates = certificateFactory.generateCertificates(Utils.stringToInputStream(cert)) val certificates = certificateFactory.generateCertificates(Utils.stringToInputStream(cert))
require(!certificates.isEmpty()) { "expected non-empty set of trusted certificates" } require(certificates.isNotEmpty()) { "expected non-empty set of trusted certificates" }
val caKeyStore = newEmptyKeyStore() val caKeyStore = newEmptyKeyStore()
certificates.forEachIndexed { index, certificate -> certificates.forEachIndexed { index, certificate ->

View File

@@ -9,23 +9,23 @@ import com.github.gotify.client.auth.ApiKeyAuth
import com.github.gotify.client.auth.HttpBasicAuth import com.github.gotify.client.auth.HttpBasicAuth
internal object ClientFactory { internal object ClientFactory {
private fun unauthorized(baseUrl: String, sslSettings: SSLSettings private fun unauthorized(baseUrl: String, sslSettings: SSLSettings): ApiClient {
): ApiClient { return defaultClient(arrayOf(), "$baseUrl/", sslSettings)
return defaultClient(arrayOfNulls(0), "$baseUrl/", sslSettings)
} }
fun basicAuth( fun basicAuth(
baseUrl: String, baseUrl: String,
sslSettings: SSLSettings, sslSettings: SSLSettings,
username: String?, username: String,
password: String? password: String
): ApiClient { ): ApiClient {
val client = defaultClient( val client = defaultClient(
arrayOf("basicAuth"), arrayOf("basicAuth"),
"$baseUrl/", sslSettings "$baseUrl/",
sslSettings
) )
val auth = client.apiAuthorizations["basicAuth"] as HttpBasicAuth? val auth = client.apiAuthorizations["basicAuth"] as HttpBasicAuth
auth!!.username = username auth.username = username
auth.password = password auth.password = password
return client return client
} }

View File

@@ -80,7 +80,7 @@ internal class InitializationActivity : AppCompatActivity() {
} }
var response = exception.body var response = exception.body
response = response.substring(0, 200.coerceAtMost(response.length)) response = response.take(200)
dialog(getString(R.string.other_error, settings.url, exception.code, response)) dialog(getString(R.string.other_error, settings.url, exception.code, response))
} }

View File

@@ -2,7 +2,6 @@ package com.github.gotify.log
import android.content.Context import android.content.Context
import com.hypertrack.hyperlog.LogFormat import com.hypertrack.hyperlog.LogFormat
import java.util.*
internal class Format(context: Context) : LogFormat(context) { internal class Format(context: Context) : LogFormat(context) {
override fun getFormattedLogMessage( override fun getFormattedLogMessage(
@@ -13,5 +12,5 @@ internal class Format(context: Context) : LogFormat(context) {
senderName: String, senderName: String,
osVersion: String, osVersion: String,
deviceUuid: String deviceUuid: String
) = String.format(Locale.ENGLISH, "%s %s: %s", timeStamp, logLevelName, message) ) = "$timeStamp $logLevelName: $message"
} }

View File

@@ -83,7 +83,7 @@ internal class LoginActivity : AppCompatActivity() {
} }
private fun doCheckUrl() { private fun doCheckUrl() {
val url = binding.gotifyUrl.text.toString() var url = binding.gotifyUrl.text.toString()
val parsedUrl = HttpUrl.parse(url) val parsedUrl = HttpUrl.parse(url)
if (parsedUrl == null) { if (parsedUrl == null) {
Utils.showSnackBar(this, "Invalid URL (include http:// or https://)") Utils.showSnackBar(this, "Invalid URL (include http:// or https://)")
@@ -97,19 +97,17 @@ internal class LoginActivity : AppCompatActivity() {
binding.checkurlProgress.visibility = View.VISIBLE binding.checkurlProgress.visibility = View.VISIBLE
binding.checkurl.visibility = View.GONE binding.checkurl.visibility = View.GONE
val trimmedUrl = url.trim() url = url.trim()
val fixedUrl = if (trimmedUrl.endsWith("/")) { if (url.endsWith("/")) url.dropLast(1)
trimmedUrl.substring(0, trimmedUrl.length - 1)
} else trimmedUrl
try { try {
ClientFactory.versionApi(fixedUrl, tempSslSettings()) ClientFactory.versionApi(url, tempSslSettings())
?.version ?.version
?.enqueue(Callback.callInUI(this, onValidUrl(fixedUrl), onInvalidUrl(fixedUrl))) ?.enqueue(Callback.callInUI(this, onValidUrl(url), onInvalidUrl(url)))
} catch (e: Exception) { } catch (e: Exception) {
binding.checkurlProgress.visibility = View.GONE binding.checkurlProgress.visibility = View.GONE
binding.checkurl.visibility = View.VISIBLE binding.checkurl.visibility = View.VISIBLE
val errorMsg = getString(R.string.version_failed, "$fixedUrl/version", e.message) val errorMsg = getString(R.string.version_failed, "$url/version", e.message)
Utils.showSnackBar(this, errorMsg) Utils.showSnackBar(this, errorMsg)
} }
} }
@@ -169,7 +167,7 @@ internal class LoginActivity : AppCompatActivity() {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
try { try {
if (requestCode == FILE_SELECT_CODE) { if (requestCode == FILE_SELECT_CODE) {
require(resultCode == RESULT_OK) { String.format("result was %d", resultCode) } require(resultCode == RESULT_OK) { "result was $resultCode" }
requireNotNull(data) { "file path was null" } requireNotNull(data) { "file path was null" }
val uri = data.data ?: throw IllegalArgumentException("file path was null") val uri = data.data ?: throw IllegalArgumentException("file path was null")
@@ -268,7 +266,7 @@ internal class LoginActivity : AppCompatActivity() {
private fun onCreatedClient(client: Client) { private fun onCreatedClient(client: Client) {
settings.token = client.token settings.token = client.token
settings.validateSSL = !disableSslValidation settings.validateSSL = !disableSslValidation
settings.cert = caCertContents.toString() settings.cert = caCertContents
Utils.showSnackBar(this, getString(R.string.created_client)) Utils.showSnackBar(this, getString(R.string.created_client))
startActivity(Intent(this, InitializationActivity::class.java)) startActivity(Intent(this, InitializationActivity::class.java))
@@ -291,6 +289,6 @@ internal class LoginActivity : AppCompatActivity() {
} }
private fun tempSslSettings(): SSLSettings { private fun tempSslSettings(): SSLSettings {
return SSLSettings(!disableSslValidation, caCertContents.toString()) return SSLSettings(!disableSslValidation, caCertContents)
} }
} }

View File

@@ -3,9 +3,7 @@ package com.github.gotify.messages
import com.github.gotify.client.model.Message import com.github.gotify.client.model.Message
internal object Extras { internal object Extras {
fun useMarkdown(message: Message): Boolean { fun useMarkdown(message: Message): Boolean = useMarkdown(message.extras)
return useMarkdown(message.extras)
}
fun useMarkdown(extras: Map<String, Any>?): Boolean { fun useMarkdown(extras: Map<String, Any>?): Boolean {
if (extras == null) { if (extras == null) {

View File

@@ -26,9 +26,9 @@ 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 com.squareup.picasso.Picasso
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import org.threeten.bp.OffsetDateTime
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
import org.threeten.bp.OffsetDateTime
internal class ListMessageAdapter( internal class ListMessageAdapter(
private val context: Context, private val context: Context,
@@ -72,13 +72,13 @@ internal class ListMessageAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val message = items[position] val message = items[position]
if (Extras.useMarkdown(message.message)) { if (Extras.useMarkdown(message.message)) {
holder.message!!.autoLinkMask = 0 holder.message.autoLinkMask = 0
markwon.setMarkdown(holder.message!!, message.message.message) markwon.setMarkdown(holder.message, message.message.message)
} else { } else {
holder.message!!.autoLinkMask = Linkify.WEB_URLS holder.message.autoLinkMask = Linkify.WEB_URLS
holder.message!!.text = message.message.message holder.message.text = message.message.message
} }
holder.title!!.text = message.message.title holder.title.text = message.message.title
picasso.load(Utils.resolveAbsoluteUrl("${settings.url}/", message.image)) picasso.load(Utils.resolveAbsoluteUrl("${settings.url}/", message.image))
.error(R.drawable.ic_alarm) .error(R.drawable.ic_alarm)
.placeholder(R.drawable.ic_placeholder) .placeholder(R.drawable.ic_placeholder)
@@ -87,10 +87,10 @@ internal class ListMessageAdapter(
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val timeFormat = prefs.getString(timeFormatPrefsKey, timeFormatRelative) val timeFormat = prefs.getString(timeFormatPrefsKey, timeFormatRelative)
holder.setDateTime(message.message.date, timeFormat == timeFormatRelative) holder.setDateTime(message.message.date, timeFormat == timeFormatRelative)
holder.date!!.setOnClickListener { holder.switchTimeFormat() } holder.date.setOnClickListener { holder.switchTimeFormat() }
holder.delete!!.setOnClickListener { holder.delete.setOnClickListener {
delete.delete(holder.adapterPosition, message.message!!, false) delete.delete(holder.adapterPosition, message.message, false)
} }
} }
@@ -102,14 +102,14 @@ internal class ListMessageAdapter(
} }
class ViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) { class ViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) {
var image: ImageView? = null lateinit var image: ImageView
var message: TextView? = null lateinit var message: TextView
var title: TextView? = null lateinit var title: TextView
var date: TextView? = null lateinit var date: TextView
var delete: ImageButton? = null lateinit var delete: ImageButton
private var relativeTimeFormat = true private var relativeTimeFormat = true
private var dateTime: OffsetDateTime? = null private lateinit var dateTime: OffsetDateTime
init { init {
enableCopyToClipboard() enableCopyToClipboard()
@@ -133,38 +133,35 @@ internal class ListMessageAdapter(
updateDate() updateDate()
} }
fun setDateTime(dateTime: OffsetDateTime?, relativeTimeFormatPreference: Boolean) { fun setDateTime(dateTime: OffsetDateTime, relativeTimeFormatPreference: Boolean) {
this.dateTime = dateTime this.dateTime = dateTime
relativeTimeFormat = relativeTimeFormatPreference relativeTimeFormat = relativeTimeFormatPreference
updateDate() updateDate()
} }
private fun updateDate() { private fun updateDate() {
var text = "?" val text = if (relativeTimeFormat) {
if (dateTime != null) {
text = if (relativeTimeFormat) {
// Relative time format // Relative time format
Utils.dateToRelative(dateTime!!) Utils.dateToRelative(dateTime)
} else { } else {
// Absolute time format // Absolute time format
val time = dateTime!!.toInstant().toEpochMilli() val time = dateTime.toInstant().toEpochMilli()
val date = Date(time) val date = Date(time)
if (DateUtils.isToday(time)) { if (DateUtils.isToday(time)) {
DateFormat.getTimeInstance(DateFormat.SHORT).format(date) DateFormat.getTimeInstance(DateFormat.SHORT).format(date)
} else { } else {
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT) DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT).format(date)
.format(date)
} }
} }
}
date!!.text = text date.text = text
} }
private fun enableCopyToClipboard() { private fun enableCopyToClipboard() {
super.itemView.setOnLongClickListener { view: View -> super.itemView.setOnLongClickListener { view: View ->
val clipboard = view.context val clipboard = view.context
.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? .getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
val clip = ClipData.newPlainText("GotifyMessageContent", message!!.text.toString()) val clip = ClipData.newPlainText("GotifyMessageContent", message.text.toString())
if (clipboard != null) { if (clipboard != null) {
clipboard.setPrimaryClip(clip) clipboard.setPrimaryClip(clip)
Toast.makeText( Toast.makeText(
@@ -178,7 +175,7 @@ internal class ListMessageAdapter(
} }
} }
interface Delete { fun interface Delete {
fun delete(position: Int, message: Message, listAnimation: Boolean) fun delete(position: Int, message: Message, listAnimation: Boolean)
} }
} }

View File

@@ -46,10 +46,8 @@ import com.github.gotify.init.InitializationActivity
import com.github.gotify.log.Log import com.github.gotify.log.Log
import com.github.gotify.log.LogsActivity import com.github.gotify.log.LogsActivity
import com.github.gotify.login.LoginActivity import com.github.gotify.login.LoginActivity
import com.github.gotify.messages.ListMessageAdapter.Delete
import com.github.gotify.messages.provider.* import com.github.gotify.messages.provider.*
import com.github.gotify.service.WebSocketService import com.github.gotify.service.WebSocketService
import com.github.gotify.service.WebSocketService.Companion.NEW_MESSAGE_BROADCAST
import com.github.gotify.settings.SettingsActivity import com.github.gotify.settings.SettingsActivity
import com.github.gotify.sharing.ShareActivity import com.github.gotify.sharing.ShareActivity
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
@@ -86,8 +84,7 @@ internal class MessagesActivity :
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMessagesBinding.inflate(layoutInflater) binding = ActivityMessagesBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
viewModel = ViewModelProvider(this, MessagesModelFactory(this)) viewModel = ViewModelProvider(this, MessagesModelFactory(this))[MessagesModel::class.java]
.get(MessagesModel::class.java)
Log.i("Entering " + javaClass.simpleName) Log.i("Entering " + javaClass.simpleName)
initDrawer() initDrawer()
val layoutManager = LinearLayoutManager(this) val layoutManager = LinearLayoutManager(this)
@@ -99,16 +96,14 @@ internal class MessagesActivity :
this, this,
viewModel.settings, viewModel.settings,
viewModel.picassoHandler.get(), viewModel.picassoHandler.get(),
emptyList(), emptyList()
object : Delete { ) { position, message, listAnimation ->
override fun delete(position: Int, message: Message, listAnimation: Boolean) {
scheduleDeletion( scheduleDeletion(
position, position,
message, message,
listAnimation listAnimation
) )
} }
})
messagesView.addItemDecoration(dividerItemDecoration) messagesView.addItemDecoration(dividerItemDecoration)
messagesView.setHasFixedSize(true) messagesView.setHasFixedSize(true)
messagesView.layoutManager = layoutManager messagesView.layoutManager = layoutManager
@@ -191,23 +186,18 @@ internal class MessagesActivity :
viewModel.targetReferences.clear() viewModel.targetReferences.clear()
updateMessagesAndStopLoading(viewModel.messages[viewModel.appId]) updateMessagesAndStopLoading(viewModel.messages[viewModel.appId])
var selectedItem = menu.findItem(R.id.nav_all_messages) var selectedItem = menu.findItem(R.id.nav_all_messages)
for (i in applications.indices) { applications.indices.forEach {
val app = applications[i] val app = applications[it]
val item = menu.add(R.id.apps, i, APPLICATION_ORDER, app.name) val item = menu.add(R.id.apps, it, 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( val t = Utils.toDrawable(resources) { icon: Drawable? ->
resources item.icon = icon
) { icon: Drawable? -> item.icon = icon } }
viewModel.targetReferences.add(t) viewModel.targetReferences.add(t)
viewModel viewModel.picassoHandler
.picassoHandler
.get() .get()
.load( .load(Utils.resolveAbsoluteUrl(viewModel.settings.url + "/", app.image))
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) .resize(100, 100)
@@ -239,10 +229,8 @@ internal class MessagesActivity :
version.text = version.text =
getString(R.string.versions, BuildConfig.VERSION_NAME, settings.serverVersion) getString(R.string.versions, BuildConfig.VERSION_NAME, settings.serverVersion)
val refreshAll = headerView.findViewById<ImageButton>(R.id.refresh_all) val refreshAll = headerView.findViewById<ImageButton>(R.id.refresh_all)
refreshAll.setOnClickListener { view: View? -> refreshAll.setOnClickListener {
onRefreshAll( onRefreshAll(it)
view
)
} }
} }
@@ -259,7 +247,7 @@ internal class MessagesActivity :
val id = item.itemId val id = item.itemId
if (item.groupId == R.id.apps) { if (item.groupId == R.id.apps) {
val app = viewModel.appsHolder.get()[id] val app = viewModel.appsHolder.get()[id]
updateAppOnDrawerClose = if (app != null) app.id else MessageState.ALL_MESSAGES updateAppOnDrawerClose = app.id
startLoading() startLoading()
binding.appBarDrawer.toolbar.subtitle = item.title binding.appBarDrawer.toolbar.subtitle = item.title
} else if (id == R.id.nav_all_messages) { } else if (id == R.id.nav_all_messages) {
@@ -273,7 +261,7 @@ internal class MessagesActivity :
.setPositiveButton(R.string.yes) { _, _ -> .setPositiveButton(R.string.yes) { _, _ ->
doLogout() doLogout()
} }
.setNegativeButton(R.string.cancel) { a, b -> } .setNegativeButton(R.string.cancel, null)
.show() .show()
} else if (id == R.id.nav_logs) { } else if (id == R.id.nav_logs) {
startActivity(Intent(this, LogsActivity::class.java)) startActivity(Intent(this, LogsActivity::class.java))
@@ -309,22 +297,22 @@ internal class MessagesActivity :
val nManager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager val nManager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
nManager.cancelAll() nManager.cancelAll()
val filter = IntentFilter() val filter = IntentFilter()
filter.addAction(NEW_MESSAGE_BROADCAST) filter.addAction(WebSocketService.NEW_MESSAGE_BROADCAST)
registerReceiver(receiver, filter) registerReceiver(receiver, filter)
launchCoroutine { launchCoroutine {
updateMissedMessages(viewModel.messages.getLastReceivedMessage()) updateMissedMessages(viewModel.messages.getLastReceivedMessage())
} }
var selectedIndex: Int = R.id.nav_all_messages var selectedIndex = R.id.nav_all_messages
val appId = viewModel.appId val appId = viewModel.appId
if (appId != MessageState.ALL_MESSAGES) { if (appId != MessageState.ALL_MESSAGES) {
val apps = viewModel.appsHolder.get() val apps = viewModel.appsHolder.get()
for (i in apps.indices) { apps.indices.forEach {
if (apps[i].id == appId) { if (apps[it].id == appId) {
selectedIndex = i selectedIndex = it
} }
} }
} }
listMessageAdapter!!.notifyDataSetChanged() listMessageAdapter.notifyDataSetChanged()
selectAppInMenu(binding.navView.menu.findItem(selectedIndex)) selectAppInMenu(binding.navView.menu.findItem(selectedIndex))
super.onResume() super.onResume()
} }
@@ -337,17 +325,26 @@ internal class MessagesActivity :
private fun selectAppInMenu(appItem: MenuItem?) { private fun selectAppInMenu(appItem: MenuItem?) {
if (appItem != null) { if (appItem != null) {
appItem.isChecked = true appItem.isChecked = true
if (appItem.itemId != R.id.nav_all_messages) binding.appBarDrawer.toolbar.subtitle = if (appItem.itemId != R.id.nav_all_messages) {
appItem.title binding.appBarDrawer.toolbar.subtitle = appItem.title
}
} }
} }
private fun scheduleDeletion(position: Int, message: Message, listAnimation: Boolean) { private fun scheduleDeletion(
position: Int,
message: Message,
listAnimation: Boolean
) {
val adapter = binding.messagesView.adapter as ListMessageAdapter val adapter = binding.messagesView.adapter as ListMessageAdapter
val messages = viewModel.messages val messages = viewModel.messages
messages.deleteLocal(message) messages.deleteLocal(message)
adapter.items = messages[viewModel.appId] adapter.items = messages[viewModel.appId]
if (listAnimation) adapter.notifyItemRemoved(position) else adapter.notifyDataSetChanged() if (listAnimation) {
adapter.notifyItemRemoved(position)
} else {
adapter.notifyDataSetChanged()
}
showDeletionSnackbar() showDeletionSnackbar()
} }
@@ -358,17 +355,19 @@ internal class MessagesActivity :
val adapter = binding.messagesView.adapter as ListMessageAdapter val adapter = binding.messagesView.adapter as ListMessageAdapter
val appId = viewModel.appId val appId = viewModel.appId
adapter.items = messages[appId] adapter.items = messages[appId]
val insertPosition = val insertPosition = if (appId == MessageState.ALL_MESSAGES) {
if (appId == MessageState.ALL_MESSAGES) deletion.allPosition else deletion.appPosition deletion.allPosition
} else {
deletion.appPosition
}
adapter.notifyItemInserted(insertPosition) adapter.notifyItemInserted(insertPosition)
} }
} }
private fun showDeletionSnackbar() { private fun showDeletionSnackbar() {
val view: View = binding.swipeRefresh val view: View = binding.swipeRefresh
val snackbar: Snackbar = val snackbar = Snackbar.make(view, R.string.snackbar_deleted, Snackbar.LENGTH_LONG)
Snackbar.make(view, R.string.snackbar_deleted, Snackbar.LENGTH_LONG) snackbar.setAction(R.string.snackbar_undo) { undoDelete() }
snackbar.setAction(R.string.snackbar_undo) { v -> undoDelete() }
snackbar.addCallback(SnackbarCallback()) snackbar.addCallback(SnackbarCallback())
snackbar.show() snackbar.show()
} }
@@ -389,8 +388,9 @@ internal class MessagesActivity :
} }
} }
private inner class SwipeToDeleteCallback(private val adapter: ListMessageAdapter) : private inner class SwipeToDeleteCallback(
ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { private val adapter: ListMessageAdapter
) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
private var icon: Drawable? private var icon: Drawable?
private val background: ColorDrawable private val background: ColorDrawable
@@ -411,9 +411,7 @@ internal class MessagesActivity :
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder target: RecyclerView.ViewHolder
): Boolean { ) = false
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition val position = viewHolder.adapterPosition
@@ -475,14 +473,15 @@ internal class MessagesActivity :
private inner class MessageListOnScrollListener : RecyclerView.OnScrollListener() { private inner class MessageListOnScrollListener : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(view: RecyclerView, scrollState: Int) {} override fun onScrollStateChanged(view: RecyclerView, scrollState: Int) {}
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
val linearLayoutManager = view.layoutManager as LinearLayoutManager? val linearLayoutManager = view.layoutManager as LinearLayoutManager?
if (linearLayoutManager != null) { if (linearLayoutManager != null) {
val lastVisibleItem = linearLayoutManager.findLastVisibleItemPosition() val lastVisibleItem = linearLayoutManager.findLastVisibleItemPosition()
val totalItemCount = view.adapter!!.itemCount val totalItemCount = view.adapter!!.itemCount
if (lastVisibleItem > totalItemCount - 15 && totalItemCount != 0 && viewModel.messages.canLoadMore( if (lastVisibleItem > totalItemCount - 15 &&
viewModel.appId totalItemCount != 0 &&
) viewModel.messages.canLoadMore(viewModel.appId)
) { ) {
if (!isLoadMore) { if (!isLoadMore) {
isLoadMore = true isLoadMore = true
@@ -540,13 +539,8 @@ internal class MessagesActivity :
client.createService(ApplicationApi::class.java) client.createService(ApplicationApi::class.java)
.deleteApp(appId) .deleteApp(appId)
.enqueue( .enqueue(
Callback.callInUI( Callback.callInUI(this, { refreshAll() }) {
this, Utils.showSnackBar(this, getString(R.string.error_delete_app))
{ refreshAll() }
) {
Utils.showSnackBar(
this, getString(R.string.error_delete_app)
)
}) })
} }

View File

@@ -4,7 +4,6 @@ import android.app.Activity
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
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.ApiClient
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
@@ -13,22 +12,14 @@ import com.github.gotify.picasso.PicassoHandler
import com.squareup.picasso.Target import com.squareup.picasso.Target
internal class MessagesModel(parentView: Activity) : ViewModel() { internal class MessagesModel(parentView: Activity) : ViewModel() {
val settings: Settings val settings = Settings(parentView)
val picassoHandler: PicassoHandler val picassoHandler = PicassoHandler(parentView, settings)
val client: ApiClient val client = ClientFactory.clientToken(settings.url, settings.sslSettings(), settings.token)
val appsHolder: ApplicationHolder val appsHolder = ApplicationHolder(parentView, client)
val messages: MessageFacade val messages = MessageFacade(client.createService(MessageApi::class.java), appsHolder)
// we need to keep the target references otherwise they get gc'ed before they can be called. // we need to keep the target references otherwise they get gc'ed before they can be called.
val targetReferences = mutableListOf<Target>() val targetReferences = mutableListOf<Target>()
var appId = MessageState.ALL_MESSAGES var appId = MessageState.ALL_MESSAGES
init {
settings = Settings(parentView)
picassoHandler = PicassoHandler(parentView, settings)
client = ClientFactory.clientToken(settings.url, settings.sslSettings(), settings.token)
appsHolder = ApplicationHolder(parentView, client)
messages = MessageFacade(client.createService(MessageApi::class.java), appsHolder)
}
} }

View File

@@ -9,13 +9,11 @@ import com.github.gotify.client.api.ApplicationApi
import com.github.gotify.client.model.Application import com.github.gotify.client.model.Application
internal class ApplicationHolder(private val activity: Activity, private val client: ApiClient) { internal class ApplicationHolder(private val activity: Activity, private val client: ApiClient) {
private var state: List<Application> = listOf() private var state = listOf<Application>()
private var onUpdate: Runnable? = null private var onUpdate: Runnable? = null
private var onUpdateFailed: Runnable? = null private var onUpdateFailed: Runnable? = null
fun wasRequested(): Boolean { fun wasRequested() = state.isNotEmpty()
return state.isNotEmpty()
}
fun request() { fun request() {
client.createService(ApplicationApi::class.java) client.createService(ApplicationApi::class.java)

View File

@@ -4,13 +4,8 @@ import com.github.gotify.client.api.MessageApi
import com.github.gotify.client.model.Message import com.github.gotify.client.model.Message
internal class MessageFacade(api: MessageApi, private val applicationHolder: ApplicationHolder) { internal class MessageFacade(api: MessageApi, private val applicationHolder: ApplicationHolder) {
private val requester: MessageRequester private val requester = MessageRequester(api)
private val state: MessageStateHolder private val state = MessageStateHolder()
init {
requester = MessageRequester(api)
state = MessageStateHolder()
}
@Synchronized @Synchronized
operator fun get(appId: Long): List<MessageWithImage> { operator fun get(appId: Long): List<MessageWithImage> {

View File

@@ -11,7 +11,7 @@ import com.github.gotify.log.Log
internal class MessageRequester(private val messageApi: MessageApi) { internal class MessageRequester(private val messageApi: MessageApi) {
fun loadMore(state: MessageState): PagedMessages? { fun loadMore(state: MessageState): PagedMessages? {
return try { return try {
Log.i("Loading more messages for " + state.appId) Log.i("Loading more messages for ${state.appId}")
if (MessageState.ALL_MESSAGES == state.appId) { if (MessageState.ALL_MESSAGES == state.appId) {
Api.execute(messageApi.getMessages(LIMIT, state.nextSince)) Api.execute(messageApi.getMessages(LIMIT, state.nextSince))
} else { } else {
@@ -24,7 +24,7 @@ internal class MessageRequester(private val messageApi: MessageApi) {
} }
fun asyncRemoveMessage(message: Message) { fun asyncRemoveMessage(message: Message) {
Log.i("Removing message with id " + message.id) Log.i("Removing message with id ${message.id}")
messageApi.deleteMessage(message.id).enqueue(Callback.call()) messageApi.deleteMessage(message.id).enqueue(Callback.call())
} }

View File

@@ -28,7 +28,7 @@ internal class PicassoDataRequestHandler : RequestHandler() {
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
if (bitmap == null) { if (bitmap == null) {
val show = if (uri.length > 50) uri.substring(0, 49) + "..." else uri val show = if (uri.length > 50) uri.take(50) + "..." else uri
val malformed = RuntimeException("Malformed data uri: $show") val malformed = RuntimeException("Malformed data uri: $show")
Log.e("Could not load image", malformed) Log.e("Could not load image", malformed)
throw malformed throw malformed

View File

@@ -14,10 +14,10 @@ import com.github.gotify.log.Log
import com.github.gotify.messages.provider.MessageImageCombiner import com.github.gotify.messages.provider.MessageImageCombiner
import com.squareup.picasso.OkHttp3Downloader import com.squareup.picasso.OkHttp3Downloader
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import okhttp3.Cache
import okhttp3.OkHttpClient
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import okhttp3.Cache
import okhttp3.OkHttpClient
internal class PicassoHandler(private val context: Context, private val settings: Settings) { internal class PicassoHandler(private val context: Context, private val settings: Settings) {
companion object { companion object {
@@ -31,7 +31,7 @@ internal class PicassoHandler(private val context: Context, private val settings
) )
private val picasso: Picasso = makePicasso() private val picasso: Picasso = makePicasso()
private val appIdToAppImage: MutableMap<Long, String> = mutableMapOf() private val appIdToAppImage = mutableMapOf<Long, String>()
private fun makePicasso(): Picasso { private fun makePicasso(): Picasso {
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
@@ -45,9 +45,7 @@ internal class PicassoHandler(private val context: Context, private val settings
} }
@Throws(IOException::class) @Throws(IOException::class)
fun getImageFromUrl(url: String?): Bitmap { fun getImageFromUrl(url: String?): Bitmap = picasso.load(url).get()
return picasso.load(url).get()
}
fun getIcon(appId: Long): Bitmap { fun getIcon(appId: Long): Bitmap {
if (appId == -1L) { if (appId == -1L) {

View File

@@ -19,7 +19,7 @@ import okhttp3.*
internal class WebSocketConnection( internal class WebSocketConnection(
private val baseUrl: String, private val baseUrl: String,
settings: SSLSettings, settings: SSLSettings,
private val token: String, private val token: String?,
private val connectivityManager: ConnectivityManager, private val connectivityManager: ConnectivityManager,
private val alarmManager: AlarmManager private val alarmManager: AlarmManager
) { ) {
@@ -33,12 +33,12 @@ internal class WebSocketConnection(
private var errorCount = 0 private var errorCount = 0
private var webSocket: WebSocket? = null private var webSocket: WebSocket? = null
private var onMessage: SuccessCallback<Message>? = null private lateinit var onMessage: SuccessCallback<Message>
private var onClose: Runnable? = null private lateinit var onClose: Runnable
private var onOpen: Runnable? = null private lateinit var onOpen: Runnable
private var onBadRequest: BadRequestRunnable? = null private lateinit var onBadRequest: BadRequestRunnable
private var onNetworkFailure: OnNetworkFailureRunnable? = null private lateinit var onNetworkFailure: OnNetworkFailureRunnable
private var onReconnected: Runnable? = null private lateinit var onReconnected: Runnable
private var state: State? = null private var state: State? = null
init { init {
@@ -149,10 +149,10 @@ internal class WebSocketConnection(
syncExec { syncExec {
state = State.Connected state = State.Connected
Log.i("WebSocket($id): opened") Log.i("WebSocket($id): opened")
onOpen!!.run() onOpen.run()
if (errorCount > 0) { if (errorCount > 0) {
onReconnected!!.run() onReconnected.run()
errorCount = 0 errorCount = 0
} }
} }
@@ -163,7 +163,7 @@ internal class WebSocketConnection(
syncExec { syncExec {
Log.i("WebSocket($id): received message $text") Log.i("WebSocket($id): received message $text")
val message = Utils.JSON.fromJson(text, Message::class.java) val message = Utils.JSON.fromJson(text, Message::class.java)
onMessage!!.onSuccess(message) onMessage.onSuccess(message)
} }
super.onMessage(webSocket, text) super.onMessage(webSocket, text)
} }
@@ -172,7 +172,7 @@ internal class WebSocketConnection(
syncExec { syncExec {
if (state == State.Connected) { if (state == State.Connected) {
Log.w("WebSocket($id): closed") Log.w("WebSocket($id): closed")
onClose!!.run() onClose.run()
} }
state = State.Disconnected state = State.Disconnected
} }
@@ -186,7 +186,7 @@ internal class WebSocketConnection(
syncExec { syncExec {
state = State.Disconnected state = State.Disconnected
if (response != null && response.code() >= 400 && response.code() <= 499) { if (response != null && response.code() >= 400 && response.code() <= 499) {
onBadRequest!!.execute(message) onBadRequest.execute(message)
close() close()
return@syncExec return@syncExec
} }
@@ -200,7 +200,7 @@ internal class WebSocketConnection(
val minutes = (errorCount * 2 - 1).coerceAtMost(20) val minutes = (errorCount * 2 - 1).coerceAtMost(20)
onNetworkFailure!!.execute(minutes) onNetworkFailure.execute(minutes)
scheduleReconnect(TimeUnit.MINUTES.toSeconds(minutes.toLong())) scheduleReconnect(TimeUnit.MINUTES.toSeconds(minutes.toLong()))
} }
super.onFailure(webSocket, t, response) super.onFailure(webSocket, t, response)
@@ -215,11 +215,11 @@ internal class WebSocketConnection(
} }
} }
internal interface BadRequestRunnable { internal fun interface BadRequestRunnable {
fun execute(message: String) fun execute(message: String)
} }
internal interface OnNetworkFailureRunnable { internal fun interface OnNetworkFailureRunnable {
fun execute(minutes: Int) fun execute(minutes: Int)
} }

View File

@@ -95,16 +95,8 @@ internal class WebSocketService : Service() {
) )
.onOpen { onOpen() } .onOpen { onOpen() }
.onClose { onClose() } .onClose { onClose() }
.onBadRequest(object : BadRequestRunnable { .onBadRequest { message -> onBadRequest(message) }
override fun execute(message: String) { .onNetworkFailure { minutes -> onNetworkFailure(minutes) }
onBadRequest(message)
}
})
.onNetworkFailure(object : OnNetworkFailureRunnable {
override fun execute(minutes: Int) {
onNetworkFailure(minutes)
}
})
.onMessage { if (it != null) onMessage(it) } .onMessage { if (it != null) onMessage(it) }
.onReconnected { notifyMissedNotifications() } .onReconnected { notifyMissedNotifications() }
.start() .start()

View File

@@ -47,7 +47,9 @@ internal class SettingsActivity : AppCompatActivity(), OnSharedPreferenceChangeL
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if (getString(R.string.setting_key_theme) == key) { if (getString(R.string.setting_key_theme) == key) {
ThemeHelper.setTheme( ThemeHelper.setTheme(
this, sharedPreferences.getString(key, getString(R.string.theme_default))!!) this,
sharedPreferences.getString(key, getString(R.string.theme_default))!!
)
} }
} }

View File

@@ -52,8 +52,11 @@ internal class ShareActivity : AppCompatActivity() {
} }
if (!settings.tokenExists()) { if (!settings.tokenExists()) {
Toast.makeText(applicationContext, R.string.not_loggedin_share, Toast.LENGTH_SHORT) Toast.makeText(
.show() applicationContext,
R.string.not_loggedin_share,
Toast.LENGTH_SHORT
).show()
finish() finish()
return return
} }