add nostr foreground service

This commit is contained in:
2026-05-18 12:34:46 +07:00
parent 955da2fea6
commit 1c85e26e7f
7 changed files with 189 additions and 24 deletions

View File

@@ -27,6 +27,7 @@ kotlin {
implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0")
implementation("su.reya:nostr-sdk-kmp:0.2.3")
implementation("io.github.kalinjul.easyqrscan:scanner:0.7.0")
implementation("androidx.lifecycle:lifecycle-process:2.8.0")
}
commonMain.dependencies {
implementation(libs.compose.runtime)

View File

@@ -7,6 +7,9 @@
android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
@@ -23,6 +26,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".NostrForegroundService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application>
</manifest>

View File

@@ -45,7 +45,7 @@ val LocalNavController = staticCompositionLocalOf<NavController> {
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun App(dbPath: String) {
fun App() {
val context = LocalContext.current
val navController = rememberNavController()
val darkMode = isSystemInDarkTheme()
@@ -53,11 +53,10 @@ fun App(dbPath: String) {
// Snackbar
val snackbarHostState = remember { SnackbarHostState() }
// Initialize Nostr and SecretStore
val nostr = remember { Nostr() }
// Initialize Nostr View Model and Secret Store
val secretStore = remember { SecretStore(context) }
val viewModel: NostrViewModel = viewModel { NostrViewModel(nostr, secretStore) }
val viewModel: NostrViewModel = viewModel { NostrViewModel(NostrManager.instance, secretStore) }
// Enabled the dynamic color scheme
val colorScheme = when {
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S -> {
@@ -69,7 +68,7 @@ fun App(dbPath: String) {
}
LaunchedEffect(Unit) {
viewModel.initAndConnect(dbPath)
viewModel.login()
viewModel.startNotificationHandler()
viewModel.getChatRooms()

View File

@@ -1,22 +1,27 @@
package su.reya.coop
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import java.io.File
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
// Get database directory
val dbDir = File(filesDir, "nostr")
dbDir.mkdirs()
val intent = Intent(this, NostrForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
setContent {
App(dbDir.absolutePath)
App()
}
}
}

View File

@@ -0,0 +1,93 @@
package su.reya.coop
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
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 = NostrManager.instance
override fun onBind(intent: Intent?): IBinder? = null
private fun isUserInApp(): Boolean {
return ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
createNotificationChannel()
val notification = createNotification("Connecting to Nostr...")
startForeground(1, notification)
serviceScope.launch {
try {
val dbDir = File(filesDir, "nostr")
dbDir.mkdirs()
// Initialize Nostr client
nostr.init(dbDir.absolutePath)
// Handle notifications
nostr.handleLiteNotifications { event ->
if (!isUserInApp()) {
showNewMessageNotification(event.content())
}
}
} catch (e: Exception) {
println("Failed to start Nostr in background: ${e.message}")
}
}
return START_STICKY
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel() {
val channel = NotificationChannel(
"nostr_service",
"Nostr Background Service",
NotificationManager.IMPORTANCE_HIGH
)
val manager = getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(channel)
}
private fun createNotification(content: String): Notification {
return NotificationCompat.Builder(this, "nostr_service")
.setContentTitle("Coop")
.setContentText(content)
.setSmallIcon(android.R.drawable.ic_menu_send)
.setOngoing(true)
.build()
}
private fun showNewMessageNotification(message: String) {
val notification = NotificationCompat.Builder(this, "nostr_service")
.setContentTitle("New Message")
.setContentText(message)
.setAutoCancel(true)
.build()
val manager = getSystemService(NotificationManager::class.java)
manager?.notify(System.currentTimeMillis().toInt(), notification)
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
}
}