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.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import su.reya.coop.nostr.NostrManager import java.io.File private const val GROUP_KEY_MESSAGES = "su.reya.coop.MESSAGES" class NostrForegroundService : Service() { private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val nostr by lazy { NostrManager.instance } private var notificationJob: Job? = null override fun onBind(intent: Intent?): IBinder? = null override fun onCreate() { super.onCreate() createNotificationChannel() startAsForeground() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent?.action == "STOP_SERVICE") { stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() return START_NOT_STICKY } // Start the nostr service in the foreground startAsForeground() // Check if the service is already running if (notificationJob?.isActive == true) return START_STICKY // Start the Nostr client notificationJob = serviceScope.launch { try { Log.d("Coop", "Starting Nostr in background") // Create a database directory val dbDir = File(filesDir, "nostr") dbDir.mkdirs() // Initialize Nostr client try { nostr.init(dbDir.absolutePath) } catch (e: Exception) { throw IllegalStateException("Failed to initialize Nostr Client", e) } // Connect to bootstrap relays nostr.connectBootstrapRelays() // Handle notifications nostr.handleNotifications( onMetadataUpdate = { pubkey, metadata -> serviceScope.launch { nostr.profiles.emitMetadataUpdate(pubkey, metadata) } }, onContactListUpdate = { contacts -> serviceScope.launch { nostr.profiles.emitContactListUpdate(contacts) } }, 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 } private fun isUserInApp(): Boolean { return ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) } private fun startAsForeground() { val notification = createNotification() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING) } else { startForeground(1, notification) } } private fun getStopServicePendingIntent(): PendingIntent { val intent = Intent(this, NostrForegroundService::class.java).apply { action = "STOP_SERVICE" } return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) } 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") .setGroup(GROUP_KEY_MESSAGES) .setSmallIcon(R.drawable.ic_notification) .setOngoing(true) .setPriority(NotificationCompat.PRIORITY_MIN) .setCategory(Notification.CATEGORY_SERVICE) .addAction(R.drawable.ic_notification, "Stop", getStopServicePendingIntent()) 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() } override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) if (!NotificationManagerCompat.from(this).areNotificationsEnabled()) { Log.d("Coop", "Stopping service on task removed because notifications are disabled") stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() } } }