Migrate crash reporter UI to Jetpack Compose

This commit is contained in:
MM20
2022-03-07 16:18:33 +01:00
parent b53aaeec4e
commit a20707fc3c
24 changed files with 456 additions and 705 deletions
@@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.component.preferences
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@@ -23,7 +24,8 @@ import de.mm20.launcher2.ui.locals.LocalNavController
fun PreferenceScreen(
title: String,
floatingActionButton: @Composable () -> Unit = {},
content: LazyListScope.() -> Unit
topBarActions: @Composable RowScope.() -> Unit = {},
content: LazyListScope.() -> Unit,
) {
val navController = LocalNavController.current
val systemUiController = rememberSystemUiController()
@@ -50,6 +52,7 @@ fun PreferenceScreen(
Icon(imageVector = Icons.Rounded.ArrowBack, contentDescription = "Back")
}
},
actions = topBarActions
)
}) {
LazyColumn(
@@ -6,10 +6,7 @@ import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.navigation.navArgument
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable
@@ -30,6 +27,8 @@ import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen
import de.mm20.launcher2.ui.settings.calendarwidget.CalendarWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.cards.CardsSettingsScreen
import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.crashreporter.CrashReportScreen
import de.mm20.launcher2.ui.settings.crashreporter.CrashReporterScreen
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
import de.mm20.launcher2.ui.settings.easteregg.EasterEggSettingsScreen
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen
@@ -55,6 +54,12 @@ class SettingsActivity : BaseActivity() {
setContent {
val navController = rememberAnimatedNavController()
LaunchedEffect(intent) {
intent.getStringExtra("de.mm20.launcher2.settings.ROUTE")
?.let { navController.navigate(it) }
}
val cardStyle by remember {
dataStore.data.map { it.cards }.distinctUntilChanged()
}.collectAsState(
@@ -127,6 +132,17 @@ class SettingsActivity : BaseActivity() {
composable("settings/debug") {
DebugSettingsScreen()
}
composable("settings/debug/crashreporter") {
CrashReporterScreen()
}
composable("settings/debug/crashreporter/report?fileName={fileName}",
arguments = listOf(navArgument("fileName") {
nullable = false
})
) {
val fileName = it.arguments?.getString("fileName")
CrashReportScreen(fileName!!)
}
composable(
"settings/license?library={libraryName}",
arguments = listOf(navArgument("libraryName") {
@@ -0,0 +1,92 @@
package de.mm20.launcher2.ui.settings.crashreporter
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.BugReport
import androidx.compose.material.icons.rounded.Share
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.crashreporter.CrashReportType
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
@Composable
fun CrashReportScreen(fileName: String) {
val viewModel: CrashReportScreenVM = viewModel()
val context = LocalContext.current
val crashReport by remember(fileName) { viewModel.getCrashReport(fileName) }.observeAsState()
PreferenceScreen(
title = when (crashReport?.type) {
CrashReportType.Exception -> "Exception"
CrashReportType.Crash -> "Crash"
null -> ""
},
topBarActions = {
IconButton(onClick = { crashReport?.let { viewModel.shareCrashReport(context, it) } }) {
Icon(imageVector = Icons.Rounded.Share, contentDescription = null)
}
if (crashReport?.type == CrashReportType.Crash) {
IconButton(onClick = { crashReport?.let { viewModel.createIssue(context, it) } }) {
Icon(imageVector = Icons.Rounded.BugReport, contentDescription = null)
}
}
}
) {
item {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
color = if (crashReport?.type == CrashReportType.Crash) {
MaterialTheme.colorScheme.errorContainer
} else {
MaterialTheme.colorScheme.primaryContainer
},
shape = RoundedCornerShape(8.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(
rememberScrollState()
),
) {
crashReport?.stacktrace?.let {
Text(
text = it,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
item {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
) {
Text(text = "Device Information", style = MaterialTheme.typography.titleMedium)
val deviceInformation = remember { viewModel.getDeviceInformation(context) }
Text(
text = deviceInformation,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
)
}
}
}
}
@@ -0,0 +1,63 @@
package de.mm20.launcher2.ui.settings.crashreporter
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import de.mm20.launcher2.crashreporter.CrashReport
import de.mm20.launcher2.crashreporter.CrashReporter
import java.io.File
import java.net.URLEncoder
class CrashReportScreenVM : ViewModel() {
fun getCrashReport(fileName: String) = liveData<CrashReport?> {
emit(CrashReporter.getCrashReport(fileName))
}
fun getDeviceInformation(context: Context): String {
return CrashReporter.getDeviceInformation(context)
}
fun createIssue(context: Context, crashReport: CrashReport) {
val stacktrace = crashReport.stacktrace?.lines()?.let {
if (it.size > 15) it.subList(0, 15)
.joinToString("\n") + "\n[${it.size - 15} lines truncated]"
else it.joinToString("\n")
} ?: ""
val body =
"## Description\n\n" +
"*Please provide as many information about the crash as possible (What did you do before the crash happened? Steps to reproduce?)*\n\n" +
"## Strack trace\n\n" +
"```\n" +
"${stacktrace}\n" +
"```\n\n" +
"## Device info\n" +
"${getDeviceInformation(context).replace("\n", "<br>")}\n"
val url = "https://github.com/MM2-0/Kvaesitso/issues/new?labels=crash+report&body=${
URLEncoder.encode(
body,
"utf8"
)
}"
context.startActivity(Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(url)
})
}
fun shareCrashReport(context: Context, crashReport: CrashReport) {
val uri = FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".fileprovider",
File(crashReport.filePath)
)
val intent = Intent(Intent.ACTION_SEND)
intent.type = "*/*"
intent.putExtra(Intent.EXTRA_TEXT, CrashReporter.getDeviceInformation(context))
intent.putExtra(Intent.EXTRA_STREAM, uri)
context.startActivity(Intent.createChooser(intent, "Share via"))
}
}
@@ -0,0 +1,121 @@
package de.mm20.launcher2.ui.settings.crashreporter
import android.text.format.DateUtils
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Error
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material.icons.rounded.WarningAmber
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.crashreporter.CrashReportType
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.locals.LocalNavController
import java.net.URLEncoder
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CrashReporterScreen() {
val viewModel: CrashReporterScreenVM = viewModel()
val navController = LocalNavController.current
val reports by viewModel.reports.observeAsState()
val showExceptions by viewModel.showExceptions.observeAsState(true)
val showCrashes by viewModel.showCrashes.observeAsState(true)
PreferenceScreen(title = stringResource(R.string.preference_crash_reporter)) {
reports?.let {
item {
Row(
modifier = Modifier.fillMaxWidth().padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
IconToggleButton(checked = showExceptions, onCheckedChange = { value ->
viewModel.setShowExceptions(value)
}) {
Icon(
imageVector = if (showExceptions) Icons.Rounded.Warning else Icons.Rounded.WarningAmber,
contentDescription = null,
modifier = Modifier.alpha(if (showExceptions) 1f else 0.5f)
)
}
IconToggleButton(checked = showCrashes, onCheckedChange = { value ->
viewModel.setShowCrashes(value)
}) {
Icon(
imageVector = if (showCrashes) Icons.Rounded.Error else Icons.Rounded.ErrorOutline,
contentDescription = null,
modifier = Modifier.alpha(if (showCrashes) 1f else 0.5f)
)
}
}
}
items(it) {
OutlinedCard(
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 8.dp)
,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clickable {
navController?.navigate("settings/debug/crashreporter/report?fileName=${it.filePath}")
}
.padding(16.dp)
) {
Text(
text = DateUtils.formatDateTime(
LocalContext.current,
it.time.time,
DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE
),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.secondary
)
Row(
modifier = Modifier.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
CompositionLocalProvider(
LocalContentColor provides if (it.type == CrashReportType.Exception) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
) {
Icon(
modifier = Modifier.padding(end = 8.dp),
imageVector = if (it.type == CrashReportType.Exception) Icons.Rounded.Warning else Icons.Rounded.Error,
contentDescription = null
)
Text(
text = if (it.type == CrashReportType.Exception) "Exception" else "Crash",
style = MaterialTheme.typography.titleMedium
)
}
}
Text(
text = it.summary,
style = MaterialTheme.typography.bodySmall,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}
}
} ?: item {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
}
}
@@ -0,0 +1,45 @@
package de.mm20.launcher2.ui.settings.crashreporter
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.crashreporter.CrashReport
import de.mm20.launcher2.crashreporter.CrashReportType
import de.mm20.launcher2.crashreporter.CrashReporter
import kotlinx.coroutines.launch
class CrashReporterScreenVM: ViewModel() {
fun setShowCrashes(showCrashes: Boolean) {
this.showCrashes.value = showCrashes
updateReports()
}
fun setShowExceptions(showExceptions: Boolean) {
this.showExceptions.value = showExceptions
updateReports()
}
private fun updateReports() {
val exceptions = showExceptions.value == true
val crashes = showCrashes.value == true
reports.value = _reports?.filter {
it.type == CrashReportType.Exception && exceptions ||
it.type == CrashReportType.Crash && crashes
}
}
val showExceptions = MutableLiveData(true)
val showCrashes = MutableLiveData(true)
val reports = MutableLiveData<List<CrashReport>?>(null)
private var _reports: List<CrashReport>? = null
init {
viewModelScope.launch {
_reports = CrashReporter.getCrashReports()
reports.value = _reports
}
}
}
@@ -12,6 +12,7 @@ import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.locals.LocalNavController
import kotlinx.coroutines.launch
import java.io.File
@@ -19,6 +20,7 @@ import java.io.File
fun DebugSettingsScreen() {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
PreferenceScreen(
stringResource(R.string.preference_screen_debug)
) {
@@ -27,7 +29,7 @@ fun DebugSettingsScreen() {
title = stringResource(R.string.preference_crash_reporter),
summary = stringResource(R.string.preference_crash_reporter_summary),
onClick = {
context.startActivity(CrashReporter.getLaunchIntent())
navController?.navigate("settings/debug/crashreporter")
})
Preference(