Files
android/app/src/main/java/sh/sar/basedbank/service/NotificationPollingService.kt
T

175 lines
6.6 KiB
Kotlin

package sh.sar.basedbank.service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.os.IBinder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.BmlNotificationsClient
import sh.sar.basedbank.api.mib.MibActivityHistoryClient
import sh.sar.basedbank.ui.home.AppNotification
import sh.sar.basedbank.util.NotificationsCache
import java.util.concurrent.atomic.AtomicInteger
class NotificationPollingService : Service() {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val app get() = application as BasedBankApp
private val notifIdCounter = AtomicInteger(2000)
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
createChannels()
startForeground(SERVICE_NOTIF_ID, buildServiceNotification())
startPolling()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = START_STICKY
override fun onDestroy() {
scope.cancel()
super.onDestroy()
}
private fun startPolling() {
scope.launch {
while (isActive) {
runCatching { poll() }
delay(POLL_INTERVAL_MS)
}
}
}
private suspend fun poll() {
pollBml()
pollMib()
}
private suspend fun pollBml() {
val sessions = app.bmlSessions.toMap()
if (sessions.isEmpty()) return
val client = BmlNotificationsClient()
sessions.forEach { (loginId, session) ->
val result = try { client.fetchNotifications(session, loginId, page = 1) }
catch (_: Exception) { return@forEach }
if (result.items.isEmpty()) return@forEach
val cached = NotificationsCache.loadBml(this@NotificationPollingService, loginId)
if (cached.isEmpty()) {
NotificationsCache.saveBml(this@NotificationPollingService, loginId, result.items)
return@forEach
}
val cachedIds = cached.map { it.id }.toSet()
val newItems = result.items.filter { it.id !in cachedIds }
if (newItems.isNotEmpty()) {
NotificationsCache.saveBml(this@NotificationPollingService, loginId, result.items)
val channelId = ensureLoginChannel("BML", loginId)
newItems.forEach { postBankNotification(it, channelId) }
}
}
}
private suspend fun pollMib() {
val sessions = app.mibSessions.toMap()
if (sessions.isEmpty()) return
val client = MibActivityHistoryClient()
sessions.forEach { (loginId, session) ->
val result = try { client.fetchActivity(session, loginId, 1, 100) }
catch (_: Exception) { return@forEach }
val readIds = NotificationsCache.getMibReadIds(this@NotificationPollingService)
val cached = NotificationsCache.loadMib(this@NotificationPollingService, loginId, readIds)
if (cached.isEmpty()) {
NotificationsCache.saveMib(this@NotificationPollingService, loginId, result.items)
return@forEach
}
val cachedIds = cached.map { it.id }.toSet()
val newItems = result.items.filter { it.id !in cachedIds }
if (newItems.isNotEmpty()) {
val all = (cached + newItems).sortedByDescending { it.timestampMs }
NotificationsCache.saveMib(this@NotificationPollingService, loginId, all)
val channelId = ensureLoginChannel("MIB", loginId)
newItems.forEach { postBankNotification(it, channelId) }
}
}
}
private fun ensureLoginChannel(bank: String, loginId: String): String {
val channelId = "bank_${bank.lowercase()}_$loginId"
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
if (nm.getNotificationChannel(channelId) == null) {
val profileName = when (bank) {
"BML" -> app.bmlProfilesMap[loginId]?.firstOrNull()?.name
"MIB" -> app.mibProfilesMap[loginId]?.firstOrNull()?.name
else -> null
} ?: loginId
nm.createNotificationChannel(
NotificationChannel(channelId, "$bank · $profileName", NotificationManager.IMPORTANCE_DEFAULT)
)
}
return channelId
}
private fun postBankNotification(notif: AppNotification, channelId: String) {
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val pi = PendingIntent.getActivity(
this, 0,
packageManager.getLaunchIntentForPackage(packageName),
PendingIntent.FLAG_IMMUTABLE
)
val n = Notification.Builder(this, channelId)
.setSmallIcon(R.drawable.ic_bell)
.setContentTitle(notif.title)
.setContentText(notif.message)
.setContentIntent(pi)
.setAutoCancel(true)
.build()
nm.notify(notifIdCounter.getAndIncrement(), n)
}
private fun buildServiceNotification(): Notification {
val pi = PendingIntent.getActivity(
this, 0,
packageManager.getLaunchIntentForPackage(packageName),
PendingIntent.FLAG_IMMUTABLE
)
return Notification.Builder(this, CHANNEL_SERVICE)
.setSmallIcon(R.drawable.ic_bell)
.setContentTitle(getString(R.string.notif_service_title))
.setContentText(getString(R.string.notif_service_desc))
.setContentIntent(pi)
.setOngoing(true)
.build()
}
private fun createChannels() {
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
nm.createNotificationChannel(
NotificationChannel(
CHANNEL_SERVICE,
getString(R.string.notif_channel_service),
NotificationManager.IMPORTANCE_MIN
).apply { setShowBadge(false) }
)
}
companion object {
private const val POLL_INTERVAL_MS = 30_000L
private const val SERVICE_NOTIF_ID = 1001
const val CHANNEL_SERVICE = "notif_polling_service"
}
}