Files
xps-geode-launcher/app/src/main/java/com/geode/launcher/ApplicationLogsActivity.kt
T
2024-01-28 19:02:13 -07:00

411 lines
16 KiB
Kotlin

package com.geode.launcher
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Divider
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.log.LogLine
import com.geode.launcher.log.LogPriority
import com.geode.launcher.log.LogViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme
import com.geode.launcher.ui.theme.Theme
import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.launch
class ApplicationLogsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
val theme = Theme.fromInt(themeOption)
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
ApplicationLogsScreen(onBackPressedDispatcher)
}
}
}
}
}
}
@Composable
fun getLogColor(priority: LogPriority): Color {
val isDark = LocalTheme.current != Theme.LIGHT
// i just copied these from my ide tbh
return if (isDark) {
when (priority) {
LogPriority.ERROR -> Color(0xff_df_51_51)
LogPriority.WARN -> Color(0xff_bc_b5_00)
LogPriority.FATAL -> Color(0xff_b8_b6_fc)
LogPriority.DEBUG -> Color(0xff_aa_aa_aa)
LogPriority.VERBOSE -> Color(0xff_77_77_77)
else -> Color.Unspecified
}
} else {
when (priority) {
LogPriority.ERROR -> Color(0xff_df_51_51)
LogPriority.WARN -> Color(0xff_ea_b7_00)
LogPriority.FATAL -> Color(0xff_a1_42_f4)
LogPriority.DEBUG -> Color(0xff_77_77_77)
LogPriority.VERBOSE -> Color(0xff_aa_aa_aa)
else -> Color.Unspecified
}
}
}
fun copyLogText(context: Context, logMessage: String) {
val clipboardManager =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboardManager.setPrimaryClip(
ClipData.newPlainText(
context.getString(R.string.application_log_copy_label),
logMessage
)
)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
Toast.makeText(
context,
context.getString(R.string.text_copied),
Toast.LENGTH_SHORT
).show()
}
}
@Composable
fun SelectLogLevelDialog(
onDismissRequest: () -> Unit,
onSelect: (LogPriority) -> Unit,
initialValue: LogPriority
) {
SelectDialog(
title = stringResource(R.string.application_logs_level_dialog_name),
onDismissRequest = onDismissRequest,
onSelect = { onSelect(it) },
initialValue = initialValue,
toLabel = {
// i'm lazy
it.name.lowercase().replaceFirstChar { c -> c.uppercaseChar() }
},
optionsCount = LogPriority.FATAL downTo LogPriority.VERBOSE
)
}
@Composable
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
val context = LocalContext.current
Column(
modifier = modifier
.padding(8.dp)
.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
copyLogText(context, logLine.asSimpleString)
}
)
},
) {
Text(
"[${logLine.priority.toChar()}] ${logLine.formattedTime}",
style = Typography.labelSmall,
color = getLogColor(logLine.priority),
fontFamily = FontFamily.Monospace
)
Text(
"[${logLine.tag}]: ${logLine.messageTrimmed}",
fontFamily = FontFamily.Monospace,
style = Typography.bodyMedium
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ApplicationLogsScreen(
onBackPressedDispatcher: OnBackPressedDispatcher?,
logViewModel: LogViewModel = viewModel()
) {
val logLines = logViewModel.lineState
val isLoading by logViewModel.isLoading.collectAsState()
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val listState = rememberLazyListState()
var showMoreOptions by remember { mutableStateOf(false) }
val saveLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
if (uri != null) {
coroutineScope.launch {
val logs = logViewModel.getLogData()
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.bufferedWriter().use { writer ->
writer.write(logs)
}
}
Toast.makeText(context, R.string.application_logs_export_completed, Toast.LENGTH_SHORT)
.show()
}
}
}
LaunchedEffect(logLines.size) {
listState.scrollToItem(logLines.size)
}
var showLogLevelSelect by remember { mutableStateOf(false) }
if (showLogLevelSelect) {
SelectLogLevelDialog(
onDismissRequest = { showLogLevelSelect = false },
onSelect = {
logViewModel.logLevel = it
showLogLevelSelect = false
},
initialValue = logViewModel.logLevel
)
}
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
Column {
TopAppBar(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_icon_alt)
)
}
},
actions = {
IconButton(onClick = {
coroutineScope.launch {
val data = logViewModel.getLogData()
val sendIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_TEXT, data)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent)
}
}, enabled = !logLines.isEmpty()) {
Icon(
Icons.Filled.Share,
contentDescription = stringResource(R.string.application_logs_share)
)
}
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
Icon(
Icons.Filled.MoreVert,
contentDescription = stringResource(R.string.application_logs_more)
)
}
DropdownMenu(
expanded = showMoreOptions,
onDismissRequest = { showMoreOptions = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_bug_report),
contentDescription = null,
)
},
trailingIcon = {
if (logViewModel.filterCrashes) {
Icon(
Icons.Filled.Check,
contentDescription = stringResource(R.string.application_logs_crash_only_enabled_alt)
)
}
},
text = {
Text(stringResource(R.string.application_logs_crash_only))
}, onClick = {
logViewModel.toggleCrashBuffer()
showMoreOptions = false
}
)
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_filter_list),
contentDescription = null,
)
},
text = {
Text(stringResource(R.string.application_logs_level))
},
onClick = {
showLogLevelSelect = true
showMoreOptions = false
}
)
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_save),
contentDescription = null,
)
},
text = {
Text(stringResource(R.string.application_logs_export))
},
onClick = {
saveLauncher.launch(context.getString(R.string.application_logs_default_filename))
showMoreOptions = false
},
enabled = !logLines.isEmpty()
)
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_delete),
contentDescription = null
)
},
text = {
Text(stringResource(R.string.application_logs_delete))
}, onClick = {
logViewModel.clearLogs()
showMoreOptions = false
}
)
}
},
title = {
Text(
stringResource(R.string.title_activity_application_logs),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
scrollBehavior = scrollBehavior,
)
if (isLoading) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
}
}
) { innerPadding ->
LazyColumn(
Modifier
.padding(innerPadding)
.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceEvenly,
state = listState
) {
if (!isLoading && logLines.isEmpty()) {
item {
Text(
stringResource(R.string.application_logs_no_logs),
textAlign = TextAlign.Center,
style = Typography.bodyLarge,
modifier = Modifier.fillMaxSize()
)
}
}
items(
logLines
) { line ->
LogCard(line)
Divider()
}
}
}
}
@Preview(showSystemUi = true)
@Composable
fun LogsPreview() {
GeodeLauncherTheme {
ApplicationLogsScreen(null)
}
}