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"