Files
coop-mobile/composeApp/src/androidMain/kotlin/su/reya/coop/NostrForegroundService.kt
2026-06-04 01:59:38 +00:00

161 lines
5.4 KiB
Kotlin

package su.reya.coop
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.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.io.File
class NostrForegroundService : Service() {
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val nostr by lazy { NostrManager.instance }
override fun onBind(intent: Intent?): IBinder? = null
private fun isUserInApp(): Boolean {
return ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
}
override fun onCreate() {
super.onCreate()
createNotificationChannel()
val notification = createNotification()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
startForeground(1, notification)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
serviceScope.launch {
try {
Log.d("Coop", "Starting Nostr in background")
// Create a database directory
val dbDir = File(filesDir, "nostr")
dbDir.mkdirs()
// Initialize Nostr client
nostr.init(dbDir.absolutePath)
// Connect to bootstrap relays
nostr.connectBootstrapRelays()
// Handle notifications
nostr.handleNotifications(
onMetadataUpdate = { pubkey, metadata ->
serviceScope.launch { nostr.emitMetadataUpdate(pubkey, metadata) }
},
onContactListUpdate = { contacts ->
serviceScope.launch { nostr.emitContactListUpdate(contacts) }
},
onSubscriptionClose = {
serviceScope.launch { nostr.emitSubscriptionClosed() }
},
onNewMessage = { event ->
serviceScope.launch {
if (!isUserInApp()) {
showNewMessageNotification(event.roomId(), event.content())
}
nostr.emitNewEvent(event)
}
}
)
} catch (e: Exception) {
Log.e("Coop", "Failed to start Nostr", e)
}
}
return START_STICKY
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel() {
val manager = getSystemService(NotificationManager::class.java)
val serviceChannel = NotificationChannel(
"nostr_service",
"Nostr Background Status",
NotificationManager.IMPORTANCE_MIN
).apply {
setShowBadge(false)
}
manager?.createNotificationChannel(serviceChannel)
val messageChannel = NotificationChannel(
"nostr_messages",
"New Messages",
NotificationManager.IMPORTANCE_HIGH
)
manager?.createNotificationChannel(messageChannel)
}
private fun createNotification(content: String? = null): Notification {
val builder = NotificationCompat.Builder(this, "nostr_service")
.setSmallIcon(R.drawable.ic_notification)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setCategory(Notification.CATEGORY_SERVICE)
if (content != null) {
builder.setContentTitle("Coop")
builder.setContentText(content)
} else {
builder.setContentTitle("Coop is active")
}
return builder.build()
}
private fun showNewMessageNotification(roomId: Long, message: String) {
val deepLinkUri = "coop://chat/$roomId".toUri()
val intent = Intent(
Intent.ACTION_VIEW,
deepLinkUri,
this,
MainActivity::class.java
)
val pendingIntent = PendingIntent.getActivity(
this,
roomId.toInt(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(this, "nostr_messages")
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("You received a new message")
.setContentText(message)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_MESSAGE)
.build()
val manager = getSystemService(NotificationManager::class.java)
manager?.notify(System.currentTimeMillis().toInt(), notification)
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
}
}