feat: add crash screen #10

Merged
reya merged 2 commits from feat/crash-screen into master 2026-06-02 02:19:14 +00:00
5 changed files with 156 additions and 8 deletions
Showing only changes of commit 930e1f6678 - Show all commits

View File

@@ -19,6 +19,12 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar"> android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:name=".CrashActivity"
android:exported="false"
android:process=":crash_handler"
android:theme="@android:style/Theme.Material.Light.NoActionBar" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

View File

@@ -0,0 +1,108 @@
package su.reya.coop
import android.content.ClipData
import android.content.ClipboardManager
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlin.system.exitProcess
class CrashActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val errorText = intent.getStringExtra("error") ?: "Unknown error"
setContent {
MaterialExpressiveTheme {
Scaffold(
content = { innerPadding ->
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")
}
}
}
}
}
)
}
}
}
}

View File

@@ -11,6 +11,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import su.reya.coop.coop.storage.SecretStore import su.reya.coop.coop.storage.SecretStore
import kotlin.system.exitProcess
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val viewModel: NostrViewModel by viewModels { private val viewModel: NostrViewModel by viewModels {
@@ -23,6 +24,26 @@ class MainActivity : ComponentActivity() {
} }
override fun onCreate(savedInstanceState: Bundle?) { 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() val splashScreen = installSplashScreen()
enableEdgeToEdge() enableEdgeToEdge()

View File

@@ -6,8 +6,10 @@ import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.net.toUri import androidx.core.net.toUri
@@ -22,7 +24,7 @@ import java.io.File
class NostrForegroundService : Service() { class NostrForegroundService : Service() {
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) 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 override fun onBind(intent: Intent?): IBinder? = null
@@ -30,18 +32,30 @@ class NostrForegroundService : Service() {
return ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel() createNotificationChannel()
} }
val notification = createNotification() 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 { serviceScope.launch {
try { try {
Log.d("Coop", "Starting Nostr in background")
// Create a database directory
val dbDir = File(filesDir, "nostr") val dbDir = File(filesDir, "nostr")
dbDir.mkdirs() dbDir.mkdirs()
// Initialize Nostr client // Initialize Nostr client
nostr.init(dbDir.absolutePath) nostr.init(dbDir.absolutePath)
// Connect to bootstrap relays // Connect to bootstrap relays
@@ -67,10 +81,9 @@ class NostrForegroundService : Service() {
} }
) )
} catch (e: Exception) { } catch (e: Exception) {
println("Failed to start Nostr in background: ${e.message}") Log.e("Coop", "Failed to start Nostr", e)
} }
} }
return START_STICKY return START_STICKY
} }

View File

@@ -1,8 +1,8 @@
[versions] [versions]
agp = "9.2.1" agp = "9.2.1"
android-compileSdk = "36" android-compileSdk = "37"
android-minSdk = "24" android-minSdk = "24"
android-targetSdk = "36" android-targetSdk = "37"
androidx-activity = "1.13.0" androidx-activity = "1.13.0"
androidx-appcompat = "1.7.1" androidx-appcompat = "1.7.1"
androidx-core = "1.18.0" androidx-core = "1.18.0"