diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2db598d56..4f76521b0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -128,6 +128,9 @@ dependencies { // gif support implementation("io.coil-kt:coil-gif:2.4.0") + // crash handling + implementation("com.github.FunkyMuse:Crashy:1.2.0") + // To use Kotlin annotation processing tool ksp("androidx.room:room-compiler:2.5.2") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a28931f54..b4b681a12 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -130,6 +130,15 @@ + + + diff --git a/app/src/main/java/com/jerboa/MainActivity.kt b/app/src/main/java/com/jerboa/MainActivity.kt index da914304c..e93f49c37 100644 --- a/app/src/main/java/com/jerboa/MainActivity.kt +++ b/app/src/main/java/com/jerboa/MainActivity.kt @@ -79,6 +79,7 @@ import com.jerboa.ui.components.report.post.CreatePostReportActivity import com.jerboa.ui.components.settings.SettingsActivity import com.jerboa.ui.components.settings.about.AboutActivity import com.jerboa.ui.components.settings.account.AccountSettingsActivity +import com.jerboa.ui.components.settings.crashlogs.CrashLogsActivity import com.jerboa.ui.components.settings.lookandfeel.LookAndFeelActivity import com.jerboa.ui.theme.JerboaTheme import com.jerboa.util.BackConfirmation.addConfirmationDialog @@ -658,6 +659,12 @@ class MainActivity : AppCompatActivity() { ) } + composable(route = Route.CRASH_LOGS) { + CrashLogsActivity( + navController = navController, + ) + } + composable( route = Route.VIEW, arguments = listOf( diff --git a/app/src/main/java/com/jerboa/Utils.kt b/app/src/main/java/com/jerboa/Utils.kt index fac8d83ab..9b411429c 100644 --- a/app/src/main/java/com/jerboa/Utils.kt +++ b/app/src/main/java/com/jerboa/Utils.kt @@ -22,6 +22,8 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.pager.PagerState import androidx.compose.material3.DrawerState +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.TabPosition import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateListOf @@ -782,6 +784,24 @@ fun scrollToTop( } } +fun showSnackbar( + scope: CoroutineScope, + snackbarHostState: SnackbarHostState, + message: String, + actionLabel: String?, + withDismissAction: Boolean = false, + snackbarDuration: SnackbarDuration, +) { + scope.launch { + snackbarHostState.showSnackbar( + message, + actionLabel, + withDismissAction, + snackbarDuration, + ) + } +} + // https://stackoverflow.com/questions/69234880/how-to-get-intent-data-in-a-composable fun Context.findActivity(): Activity? = when (this) { is Activity -> this diff --git a/app/src/main/java/com/jerboa/ui/components/common/AppBars.kt b/app/src/main/java/com/jerboa/ui/components/common/AppBars.kt index 5b713b7c8..64097a31c 100644 --- a/app/src/main/java/com/jerboa/ui/components/common/AppBars.kt +++ b/app/src/main/java/com/jerboa/ui/components/common/AppBars.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController import com.jerboa.R import com.jerboa.datatypes.samplePerson import com.jerboa.datatypes.samplePost @@ -59,6 +60,7 @@ fun SimpleTopAppBar( text: String, navController: NavController, scrollBehavior: TopAppBarScrollBehavior? = null, + actions: @Composable RowScope.() -> Unit = {}, ) { TopAppBar( scrollBehavior = scrollBehavior, @@ -75,9 +77,18 @@ fun SimpleTopAppBar( ) } }, + actions = actions, ) } +@Preview +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SimpleTopAppBarPreview() { + SimpleTopAppBar(text = "Preview", navController = rememberNavController()) { + } +} + @Composable fun BottomAppBarAll( selectedTab: NavTab, @@ -552,5 +563,7 @@ fun Modifier.simpleVerticalScrollbar( fun LoadingBar( padding: PaddingValues = PaddingValues(0.dp), ) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(padding).testTag("jerboa:loading")) + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth().padding(padding).testTag("jerboa:loading"), + ) } diff --git a/app/src/main/java/com/jerboa/ui/components/common/Navigation.kt b/app/src/main/java/com/jerboa/ui/components/common/Navigation.kt index 73cdee211..06b712244 100644 --- a/app/src/main/java/com/jerboa/ui/components/common/Navigation.kt +++ b/app/src/main/java/com/jerboa/ui/components/common/Navigation.kt @@ -153,6 +153,8 @@ fun NavController.toLookAndFeel() = navigate(Route.LOOK_AND_FEEL) fun NavController.toAbout() = navigate(Route.ABOUT) +fun NavController.toCrashLogs() = navigate(Route.CRASH_LOGS) + fun NavController.toView(url: String) { val encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8.name()) navigate(Route.ViewArgs.makeRoute(encodedUrl)) diff --git a/app/src/main/java/com/jerboa/ui/components/common/Route.kt b/app/src/main/java/com/jerboa/ui/components/common/Route.kt index 1f97669dc..d3488295f 100644 --- a/app/src/main/java/com/jerboa/ui/components/common/Route.kt +++ b/app/src/main/java/com/jerboa/ui/components/common/Route.kt @@ -41,6 +41,7 @@ object Route { const val LOOK_AND_FEEL = "lookAndFeel" const val ACCOUNT_SETTINGS = "accountSettings" const val ABOUT = "about" + const val CRASH_LOGS = "crashLogs" val VIEW = ViewArgs.route diff --git a/app/src/main/java/com/jerboa/ui/components/settings/about/AboutActivity.kt b/app/src/main/java/com/jerboa/ui/components/settings/about/AboutActivity.kt index 27156ff72..b0b916e26 100644 --- a/app/src/main/java/com/jerboa/ui/components/settings/about/AboutActivity.kt +++ b/app/src/main/java/com/jerboa/ui/components/settings/about/AboutActivity.kt @@ -32,6 +32,7 @@ import com.alorma.compose.settings.ui.SettingsMenuLink import com.jerboa.R import com.jerboa.openLink import com.jerboa.ui.components.common.SimpleTopAppBar +import com.jerboa.ui.components.common.toCrashLogs const val githubUrl = "https://github.com/dessalines/jerboa" const val jerboaMatrixChat = "https://matrix.to/#/#jerboa-dev:matrix.org" @@ -97,6 +98,16 @@ fun AboutActivity( openLink("$githubUrl/issues") }, ) + SettingsMenuLink( + title = { Text(stringResource(R.string.crash_logs)) }, + icon = { + Icon( + imageVector = Icons.Outlined.Build, + contentDescription = null, + ) + }, + onClick = { navController.toCrashLogs() }, + ) SettingsMenuLink( title = { Text(stringResource(R.string.settings_about_developer_matrix_chatroom)) }, icon = { diff --git a/app/src/main/java/com/jerboa/ui/components/settings/crashlogs/CrashLogsActivity.kt b/app/src/main/java/com/jerboa/ui/components/settings/crashlogs/CrashLogsActivity.kt new file mode 100644 index 000000000..662c6d4b4 --- /dev/null +++ b/app/src/main/java/com/jerboa/ui/components/settings/crashlogs/CrashLogsActivity.kt @@ -0,0 +1,161 @@ +package com.jerboa.ui.components.settings.crashlogs + +import android.content.Context +import android.util.Log +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.material.icons.Icons +import androidx.compose.material.icons.outlined.ContentCopy +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.crazylegend.crashyreporter.CrashyReporter +import com.jerboa.R +import com.jerboa.copyToClipboard +import com.jerboa.showSnackbar +import com.jerboa.ui.components.common.SimpleTopAppBar +import com.jerboa.ui.theme.MEDIUM_PADDING +import com.jerboa.ui.theme.SMALL_PADDING + +@Composable +fun CrashLogsActivity( + navController: NavController, +) { + Log.d("jerboa", "Got to Crash log activity") + + val ctx = LocalContext.current + + val crashes = CrashyReporter.getLogsAsStrings()?.toMutableStateList() ?: run { mutableStateListOf() } + + CrashLogs(ctx = ctx, navController = navController, crashes = crashes) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CrashLogs(ctx: Context, navController: NavController, crashes: MutableList) { + val scope = rememberCoroutineScope() + + val snackbarHostState = remember { SnackbarHostState() } + val deleteMessage = stringResource(R.string.crash_logs_all_deleted) + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + topBar = { + SimpleTopAppBar( + text = stringResource(R.string.crash_logs), + navController = navController, + actions = { + IconButton( + onClick = { + CrashyReporter.purgeLogs() + crashes.clear() + showSnackbar( + scope, + snackbarHostState, + deleteMessage, + null, + true, + SnackbarDuration.Short, + ) + }, + ) { + Icon( + Icons.Outlined.Delete, + contentDescription = stringResource(R.string.crash_logs_delete), + ) + } + }, + ) + }, + content = { padding -> + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(padding), + ) { + crashes.forEachIndexed { _, crash -> + CrashLog(ctx = ctx, crash = crash) + } + } + }, + ) +} + +@Composable +fun CrashLog(ctx: Context, crash: String) { + var expanded by remember { mutableStateOf(false) } + val textModifier = Modifier.clickable(onClick = { expanded = !expanded }) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(MEDIUM_PADDING), + ) { + Column( + modifier = Modifier.padding(MEDIUM_PADDING), + ) { + IconButton(onClick = { + copyToClipboard(ctx, crash, "crash") + }) { + Icon( + Icons.Outlined.ContentCopy, + contentDescription = stringResource(R.string.crash_logs_copy), + ) + } + } + if (expanded) { + Text( + text = crash, + modifier = textModifier, + ) + } else { + Text( + text = crash, + maxLines = 4, + overflow = TextOverflow.Ellipsis, + modifier = textModifier, + ) + } + } + Divider(modifier = Modifier.padding(bottom = SMALL_PADDING)) +} + +@Preview +@Composable +fun CrashLogsPreview() { + CrashLogs( + ctx = LocalContext.current, + navController = rememberNavController(), + crashes = mutableListOf( + "A really bad one\nlots\nof\ntrace\nlines\nhere", + "NullPointerException!", + ), + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 72fe613a5..442c0f041 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -102,6 +102,10 @@ Sort by Subscribe %1$s users / month + Crash Logs + All crash logs deleted + Copy crash log + Delete all crash logs Select community from list a title here.... Community