Rewrite 'service' to Kotlin

This commit is contained in:
Niko Diamadis
2022-11-02 16:46:17 +01:00
parent 637e8802a4
commit 47bee618b4
5 changed files with 617 additions and 645 deletions

View File

@@ -0,0 +1,384 @@
package com.github.gotify.service
import android.app.*
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Color
import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build
import android.os.IBinder
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import com.github.gotify.*
import com.github.gotify.api.Callback
import com.github.gotify.api.ClientFactory
import com.github.gotify.client.api.MessageApi
import com.github.gotify.client.model.Message
import com.github.gotify.log.Log
import com.github.gotify.log.UncaughtExceptionHandler
import com.github.gotify.messages.Extras
import com.github.gotify.messages.MessagesActivity
import com.github.gotify.picasso.PicassoHandler
import com.github.gotify.service.WebSocketConnection.BadRequestRunnable
import com.github.gotify.service.WebSocketConnection.OnNetworkFailureRunnable
import io.noties.markwon.Markwon
import java.util.concurrent.atomic.AtomicLong
class WebSocketService : Service() {
companion object {
val NEW_MESSAGE_BROADCAST = "${WebSocketService::class.java.name}.NEW_MESSAGE"
private const val NOT_LOADED = -2L
}
private lateinit var settings: Settings
private var connection: WebSocketConnection? = null
private val lastReceivedMessage = AtomicLong(NOT_LOADED)
private lateinit var missingMessageUtil: MissedMessageUtil
private lateinit var picassoHandler: PicassoHandler
private lateinit var markwon: Markwon
override fun onCreate() {
super.onCreate()
settings = Settings(this)
val client = ClientFactory.clientToken(
settings.url(),
settings.sslSettings(),
settings.token()
)
missingMessageUtil = MissedMessageUtil(client.createService(MessageApi::class.java))
Log.i("Create ${javaClass.simpleName}")
picassoHandler = PicassoHandler(this, settings)
markwon = MarkwonFactory.createForNotification(this, picassoHandler.get())
}
override fun onDestroy() {
super.onDestroy()
if (connection != null) {
connection!!.close()
}
Log.w("Destroy ${javaClass.simpleName}")
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Log.init(this)
if (connection != null) {
connection!!.close()
}
Log.i("Starting ${javaClass.simpleName}")
super.onStartCommand(intent, flags, startId)
Thread { startPushService() }.start()
return START_STICKY
}
private fun startPushService() {
UncaughtExceptionHandler.registerCurrentThread()
showForegroundNotification(getString(R.string.websocket_init))
if (lastReceivedMessage.get() == NOT_LOADED) {
missingMessageUtil.lastReceivedMessage { lastReceivedMessage.set(it) }
}
val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
connection = WebSocketConnection(
settings.url(),
settings.sslSettings(),
settings.token(),
cm,
alarmManager
)
.onOpen { onOpen() }
.onClose { onClose() }
.onBadRequest(object : BadRequestRunnable {
override fun execute(message: String) {
onBadRequest(message)
}
})
.onNetworkFailure(object : OnNetworkFailureRunnable {
override fun execute(minutes: Int) {
onNetworkFailure(minutes)
}
})
.onMessage { onMessage(it) }
.onReconnected { notifyMissedNotifications() }
.start()
val intentFilter = IntentFilter()
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
picassoHandler.updateAppIds()
}
private fun onClose() {
showForegroundNotification(
getString(R.string.websocket_closed), getString(R.string.websocket_reconnect)
)
ClientFactory.userApiWithToken(settings)
.currentUser()
.enqueue(Callback.call({ doReconnect() }) { exception ->
if (exception.code() == 401) {
showForegroundNotification(
getString(R.string.user_action),
getString(R.string.websocket_closed_logout)
)
} else {
Log.i("WebSocket closed but the user still authenticated, trying to reconnect")
doReconnect()
}
})
}
private fun doReconnect() {
if (connection == null) {
return
}
connection!!.scheduleReconnect(15)
}
private fun onBadRequest(message: String) {
showForegroundNotification(getString(R.string.websocket_could_not_connect), message)
}
private fun onNetworkFailure(minutes: Int) {
val status = getString(R.string.websocket_not_connected)
val intervalUnit = resources
.getQuantityString(R.plurals.websocket_retry_interval, minutes, minutes)
showForegroundNotification(
status, "${getString(R.string.websocket_reconnect)} $intervalUnit"
)
}
private fun onOpen() {
showForegroundNotification(getString(R.string.websocket_listening))
}
private fun notifyMissedNotifications() {
val messageId = lastReceivedMessage.get()
if (messageId == NOT_LOADED) {
return
}
val messages = missingMessageUtil.missingMessages(messageId)
if (messages.size > 5) {
onGroupedMessages(messages)
} else {
for (message in messages) {
onMessage(message)
}
}
}
private fun onGroupedMessages(messages: List<Message>) {
var highestPriority = 0L
for (message in messages) {
if (lastReceivedMessage.get() < message.id) {
lastReceivedMessage.set(message.id)
highestPriority = highestPriority.coerceAtLeast(message.priority)
}
broadcast(message)
}
val size = messages.size
showNotification(
NotificationSupport.ID.GROUPED,
getString(R.string.missed_messages),
getString(R.string.grouped_message, size),
highestPriority,
null
)
}
private fun onMessage(message: Message) {
if (lastReceivedMessage.get() < message.id) {
lastReceivedMessage.set(message.id)
}
broadcast(message)
showNotification(
message.id,
message.title,
message.message,
message.priority,
message.extras,
message.appid
)
}
private fun broadcast(message: Message) {
val intent = Intent()
intent.action = NEW_MESSAGE_BROADCAST
intent.putExtra("message", Utils.JSON.toJson(message))
sendBroadcast(intent)
}
override fun onBind(intent: Intent): IBinder? = null
private fun showForegroundNotification(title: String, message: String? = null) {
val notificationIntent = Intent(this, MessagesActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
)
val notificationBuilder =
NotificationCompat.Builder(this, NotificationSupport.Channel.FOREGROUND)
notificationBuilder.setSmallIcon(R.drawable.ic_gotify)
notificationBuilder.setOngoing(true)
notificationBuilder.priority = NotificationCompat.PRIORITY_MIN
notificationBuilder.setShowWhen(false)
notificationBuilder.setWhen(0)
notificationBuilder.setContentTitle(title)
if (message != null) {
notificationBuilder.setContentText(message)
notificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(message))
}
notificationBuilder.setContentIntent(pendingIntent)
notificationBuilder.color = ContextCompat.getColor(applicationContext, R.color.colorPrimary)
startForeground(NotificationSupport.ID.FOREGROUND, notificationBuilder.build())
}
private fun showNotification(
id: Int,
title: String,
message: String,
priority: Long,
extras: Map<String, Any>?
) {
showNotification(id.toLong(), title, message, priority, extras, -1L)
}
private fun showNotification(
id: Long,
title: String,
message: String,
priority: Long,
extras: Map<String, Any>?,
appId: Long
) {
var intent: Intent
val intentUrl = Extras.getNestedValue(
String::class.java,
extras,
"android::action",
"onReceive",
"intentUrl"
)
if (intentUrl != null) {
intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(intentUrl)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
}
val url = Extras.getNestedValue(
String::class.java,
extras,
"client::notification",
"click",
"url"
)
if (url != null) {
intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
} else {
intent = Intent(this, MessagesActivity::class.java)
}
val contentIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val b = NotificationCompat.Builder(
this, NotificationSupport.convertPriorityToChannel(priority)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
showNotificationGroup(priority)
}
b.setAutoCancel(true)
.setDefaults(Notification.DEFAULT_ALL)
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_gotify)
.setLargeIcon(picassoHandler.getIcon(appId))
.setTicker("${getString(R.string.app_name)} - $title")
.setGroup(NotificationSupport.Group.MESSAGES)
.setContentTitle(title)
.setDefaults(Notification.DEFAULT_LIGHTS or Notification.DEFAULT_SOUND)
.setLights(Color.CYAN, 1000, 5000)
.setColor(ContextCompat.getColor(applicationContext, R.color.colorPrimary))
.setContentIntent(contentIntent)
var formattedMessage = message as CharSequence
lateinit var newMessage: String
if (Extras.useMarkdown(extras)) {
formattedMessage = markwon.toMarkdown(message)
newMessage = formattedMessage.toString()
}
b.setContentText(newMessage)
b.setStyle(NotificationCompat.BigTextStyle().bigText(formattedMessage))
val notificationImageUrl = Extras.getNestedValue(
String::class.java,
extras,
"client::notification",
"bigImageUrl"
)
if (notificationImageUrl != null) {
try {
b.setStyle(
NotificationCompat.BigPictureStyle()
.bigPicture(picassoHandler.getImageFromUrl(notificationImageUrl))
)
} catch (e: Exception) {
Log.e("Error loading bigImageUrl", e)
}
}
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(Utils.longToInt(id), b.build())
}
@RequiresApi(Build.VERSION_CODES.N)
fun showNotificationGroup(priority: Long) {
val intent = Intent(this, MessagesActivity::class.java)
val contentIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(
this, NotificationSupport.convertPriorityToChannel(priority)
)
builder.setAutoCancel(true)
.setDefaults(Notification.DEFAULT_ALL)
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_gotify)
.setTicker(getString(R.string.app_name))
.setGroup(NotificationSupport.Group.MESSAGES)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
.setContentTitle(getString(R.string.grouped_notification_text))
.setGroupSummary(true)
.setContentText(getString(R.string.grouped_notification_text))
.setColor(ContextCompat.getColor(applicationContext, R.color.colorPrimary))
.setContentIntent(contentIntent)
val notificationManager = this.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(-5, builder.build())
}
}