Move source files from 'java' to 'kotlin' folder
This commit is contained in:
@@ -0,0 +1,398 @@
|
||||
package com.github.gotify.service
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
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.MarkwonFactory
|
||||
import com.github.gotify.MissedMessageUtil
|
||||
import com.github.gotify.NotificationSupport
|
||||
import com.github.gotify.R
|
||||
import com.github.gotify.Settings
|
||||
import com.github.gotify.Utils
|
||||
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 io.noties.markwon.Markwon
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
internal 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 { message -> onBadRequest(message) }
|
||||
.onNetworkFailure { minutes -> onNetworkFailure(minutes) }
|
||||
.onMessage { message -> onMessage(message) }
|
||||
.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(
|
||||
onSuccess = { doReconnect() },
|
||||
onError = { 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).filterNotNull()
|
||||
|
||||
if (messages.size > 5) {
|
||||
onGroupedMessages(messages)
|
||||
} else {
|
||||
messages.forEach {
|
||||
onMessage(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onGroupedMessages(messages: List<Message>) {
|
||||
var highestPriority = 0L
|
||||
messages.forEach { message ->
|
||||
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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user