diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml
index 89f75cb..c4d8237 100644
--- a/composeApp/src/androidMain/AndroidManifest.xml
+++ b/composeApp/src/androidMain/AndroidManifest.xml
@@ -19,6 +19,12 @@
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
+
+
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Column {
+ Text(
+ "App Crashed",
+ style = MaterialTheme.typography.titleMediumEmphasized,
+ color = MaterialTheme.colorScheme.error
+ )
+ Text(
+ "Please copy the log below and send it to the developer.",
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState())
+ ) {
+ Text(
+ text = errorText,
+ fontFamily = FontFamily.Monospace,
+ fontSize = 12.sp
+ )
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ FilledTonalButton(
+ onClick = {
+ finish();
+ exitProcess(0)
+ },
+ modifier = Modifier.weight(1f)
+ ) {
+ Text("Exit")
+ }
+ Button(
+ onClick = {
+ val clipboard =
+ getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
+ val data = ClipData.newPlainText("Crash Log", errorText)
+ clipboard.setPrimaryClip(data)
+ },
+ modifier = Modifier.weight(1f)
+ ) {
+ Text("Copy")
+ }
+ }
+ }
+ }
+ }
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/MainActivity.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/MainActivity.kt
index 0f7dffc..58a9af1 100644
--- a/composeApp/src/androidMain/kotlin/su/reya/coop/MainActivity.kt
+++ b/composeApp/src/androidMain/kotlin/su/reya/coop/MainActivity.kt
@@ -11,6 +11,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import su.reya.coop.coop.storage.SecretStore
+import kotlin.system.exitProcess
class MainActivity : ComponentActivity() {
private val viewModel: NostrViewModel by viewModels {
@@ -23,6 +24,26 @@ class MainActivity : ComponentActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
+ Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
+ throwable.printStackTrace()
+ android.util.Log.e(
+ "CoopCrash",
+ "Uncaught exception in thread ${thread.name}",
+ throwable
+ )
+
+ // Start the Crash Activity
+ val intent = Intent(this, CrashActivity::class.java).apply {
+ putExtra("error", throwable.stackTraceToString())
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ }
+ startActivity(intent)
+
+ // Exit
+ android.os.Process.killProcess(android.os.Process.myPid())
+ exitProcess(1)
+ }
+
val splashScreen = installSplashScreen()
enableEdgeToEdge()
diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/NostrForegroundService.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/NostrForegroundService.kt
index 41e99f8..03a45ee 100644
--- a/composeApp/src/androidMain/kotlin/su/reya/coop/NostrForegroundService.kt
+++ b/composeApp/src/androidMain/kotlin/su/reya/coop/NostrForegroundService.kt
@@ -6,8 +6,10 @@ 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
@@ -22,7 +24,7 @@ import java.io.File
class NostrForegroundService : Service() {
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
- private val nostr = NostrManager.instance
+ private val nostr by lazy { NostrManager.instance }
override fun onBind(intent: Intent?): IBinder? = null
@@ -30,18 +32,30 @@ class NostrForegroundService : Service() {
return ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
}
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ override fun onCreate() {
+ super.onCreate()
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel()
}
-
val notification = createNotification()
- startForeground(1, notification)
+ 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
@@ -67,10 +81,9 @@ class NostrForegroundService : Service() {
}
)
} catch (e: Exception) {
- println("Failed to start Nostr in background: ${e.message}")
+ Log.e("Coop", "Failed to start Nostr", e)
}
}
-
return START_STICKY
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a11816a..177bed6 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,8 +1,8 @@
[versions]
agp = "9.2.1"
-android-compileSdk = "36"
+android-compileSdk = "37"
android-minSdk = "24"
-android-targetSdk = "36"
+android-targetSdk = "37"
androidx-activity = "1.13.0"
androidx-appcompat = "1.7.1"
androidx-core = "1.18.0"
diff --git a/shared/src/commonMain/kotlin/su/reya/coop/CoopWebSocket.kt b/shared/src/commonMain/kotlin/su/reya/coop/CoopWebSocket.kt
deleted file mode 100644
index ff2a658..0000000
--- a/shared/src/commonMain/kotlin/su/reya/coop/CoopWebSocket.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package su.reya.coop
-
-import io.ktor.client.HttpClient
-import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession
-import io.ktor.client.plugins.websocket.webSocketSession
-import io.ktor.client.request.url
-import io.ktor.websocket.Frame
-import io.ktor.websocket.close
-import io.ktor.websocket.readBytes
-import io.ktor.websocket.readText
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import rust.nostr.sdk.ConnectionMode
-import rust.nostr.sdk.CustomWebSocketTransport
-import rust.nostr.sdk.WebSocketAdapter
-import rust.nostr.sdk.WebSocketAdapterWrapper
-import rust.nostr.sdk.WebSocketMessage
-
-class KtorWebSocketAdapter(
- private val client: HttpClient,
- private val session: DefaultClientWebSocketSession
-) : WebSocketAdapter {
-
- override suspend fun send(msg: WebSocketMessage) {
- try {
- when (msg) {
- is WebSocketMessage.Text -> session.send(Frame.Text(msg.text))
- is WebSocketMessage.Binary -> session.send(Frame.Binary(true, msg.bytes))
- is WebSocketMessage.Ping -> session.send(Frame.Ping(msg.bytes))
- is WebSocketMessage.Pong -> session.send(Frame.Pong(msg.bytes))
- else -> {}
- }
- } catch (e: Exception) {
- println("Attempted to send on a closed WebSocket: ${e.message}")
- throw e
- }
- }
-
- override suspend fun recv(): WebSocketMessage? {
- return try {
- when (val frame = session.incoming.receive()) {
- is Frame.Text -> WebSocketMessage.Text(frame.readText())
- is Frame.Binary -> WebSocketMessage.Binary(frame.readBytes())
- is Frame.Ping -> WebSocketMessage.Ping(frame.readBytes())
- is Frame.Pong -> WebSocketMessage.Pong(frame.readBytes())
- else -> null
- }
- } catch (e: ClosedReceiveChannelException) {
- null
- } catch (e: Exception) {
- throw e
- }
- }
-
- override suspend fun closeConnection() {
- session.cancel()
- session.close()
- }
-}
-
-class CoopWebSocketClient(private val httpClient: HttpClient) : CustomWebSocketTransport {
- override fun supportPing(): Boolean = false
-
- override suspend fun connect(url: String, mode: ConnectionMode): WebSocketAdapterWrapper {
- try {
- val session = httpClient.webSocketSession {
- url(url)
- }
- val adapter = KtorWebSocketAdapter(httpClient, session)
- return WebSocketAdapterWrapper(adapter)
- } catch (e: Exception) {
- throw e
- }
- }
-}
diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt
index e098895..8bccc85 100644
--- a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt
+++ b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt
@@ -2,7 +2,6 @@ package su.reya.coop
import io.ktor.client.HttpClient
import io.ktor.client.call.body
-import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.Job
@@ -106,17 +105,16 @@ class Nostr {
// Initialize the logger for nostr client
initLogger(LogLevel.DEBUG)
+ // Initialize the database and gossip instance
val lmdb = NostrDatabase.lmdb(dbPath)
val gossip = NostrGossip.inMemory()
+
+ // Set the idle timeout for relays
val idleTimeout = Duration.parse("5m")
- val httpClient = HttpClient {
- install(WebSockets)
- }
client =
ClientBuilder()
.signer(signer)
- .websocketTransport(CoopWebSocketClient(httpClient))
.database(lmdb)
.gossip(gossip)
.gossipConfig(