Skip to content

Commit badd119

Browse files
committed
Improve report export
1 parent ba8dfe6 commit badd119

10 files changed

Lines changed: 224 additions & 66 deletions

File tree

app/src/main/java/io/nekohasekai/sfa/bg/CrashReportManager.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ object CrashReportManager {
200200

201201
fun hasConfigFile(report: CrashReport): Boolean = File(report.directory, CONFIG_FILE_NAME).exists()
202202

203-
suspend fun createZipArchive(report: CrashReport, includeConfig: Boolean): File = withContext(Dispatchers.IO) {
203+
suspend fun createZipArchive(report: CrashReport, includeConfig: Boolean, includeLog: Boolean): File = withContext(Dispatchers.IO) {
204204
val cacheDir = File(Application.application.cacheDir, CRASH_REPORTS_DIR_NAME)
205205
cacheDir.mkdirs()
206206
val zipFile = File(cacheDir, "${report.id}.zip")
@@ -212,6 +212,10 @@ object CrashReportManager {
212212
if (!includeConfig) {
213213
File(strippedDir, CONFIG_FILE_NAME).delete()
214214
}
215+
if (!includeLog) {
216+
File(strippedDir, GO_LOG_FILE_NAME).delete()
217+
File(strippedDir, JVM_LOG_FILE_NAME).delete()
218+
}
215219
Libbox.createZipArchive(strippedDir.path, zipFile.path)
216220
zipFile
217221
}

app/src/main/java/io/nekohasekai/sfa/bg/OOMReportManager.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ data class OOMReportFile(
2929
enum class Kind {
3030
METADATA,
3131
CONFIG,
32+
GO_LOG,
3233
PROFILE,
3334
}
3435
}
3536

3637
object OOMReportManager {
3738
private const val METADATA_FILE_NAME = "metadata.json"
3839
private const val CONFIG_FILE_NAME = "configuration.json"
40+
private const val GO_LOG_FILE_NAME = "go.log"
3941
private const val CMDLINE_FILE_NAME = "cmdline"
4042
private const val READ_MARKER_FILE_NAME = ".read"
4143
private const val OOM_REPORTS_DIR_NAME = "oom_reports"
@@ -86,10 +88,15 @@ object OOMReportManager {
8688
if (configFile.exists()) {
8789
files.add(OOMReportFile(OOMReportFile.Kind.CONFIG, "Configuration", configFile))
8890
}
91+
val goLogFile = File(report.directory, GO_LOG_FILE_NAME)
92+
if (goLogFile.exists()) {
93+
files.add(OOMReportFile(OOMReportFile.Kind.GO_LOG, "Log", goLogFile))
94+
}
8995
report.directory.listFiles()?.filter { file ->
9096
file.isFile &&
9197
file.name != METADATA_FILE_NAME &&
9298
file.name != CONFIG_FILE_NAME &&
99+
file.name != GO_LOG_FILE_NAME &&
93100
file.name != CMDLINE_FILE_NAME &&
94101
file.name != READ_MARKER_FILE_NAME
95102
}?.sortedBy { it.name }?.forEach { file ->
@@ -133,7 +140,9 @@ object OOMReportManager {
133140

134141
fun hasConfigFile(report: OOMReport): Boolean = File(report.directory, CONFIG_FILE_NAME).exists()
135142

136-
suspend fun createZipArchive(report: OOMReport, includeConfig: Boolean): File = withContext(Dispatchers.IO) {
143+
fun hasLogFile(report: OOMReport): Boolean = File(report.directory, GO_LOG_FILE_NAME).exists()
144+
145+
suspend fun createZipArchive(report: OOMReport, includeConfig: Boolean, includeLog: Boolean): File = withContext(Dispatchers.IO) {
137146
val cacheDir = File(Application.application.cacheDir, OOM_REPORTS_DIR_NAME)
138147
cacheDir.mkdirs()
139148
val zipFile = File(cacheDir, "${report.id}.zip")
@@ -145,6 +154,9 @@ object OOMReportManager {
145154
if (!includeConfig) {
146155
File(strippedDir, CONFIG_FILE_NAME).delete()
147156
}
157+
if (!includeLog) {
158+
File(strippedDir, GO_LOG_FILE_NAME).delete()
159+
}
148160
Libbox.createZipArchive(strippedDir.path, zipFile.path)
149161
zipFile
150162
}

app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/CrashReportDetailScreen.kt

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.nekohasekai.sfa.compose.screen.tools
22

33
import android.content.Intent
4+
import androidx.activity.compose.rememberLauncherForActivityResult
5+
import androidx.activity.result.contract.ActivityResultContracts
46
import androidx.compose.foundation.background
57
import androidx.compose.foundation.clickable
68
import androidx.compose.foundation.layout.Box
@@ -23,8 +25,6 @@ import androidx.compose.material.icons.outlined.Settings
2325
import androidx.compose.material3.Card
2426
import androidx.compose.material3.CardDefaults
2527
import androidx.compose.material3.CircularProgressIndicator
26-
import androidx.compose.material3.DropdownMenu
27-
import androidx.compose.material3.DropdownMenuItem
2828
import androidx.compose.material3.ExperimentalMaterial3Api
2929
import androidx.compose.material3.Icon
3030
import androidx.compose.material3.IconButton
@@ -61,6 +61,7 @@ import kotlinx.coroutines.Dispatchers
6161
import kotlinx.coroutines.launch
6262
import kotlinx.coroutines.withContext
6363
import org.json.JSONObject
64+
import java.io.File
6465
import java.text.DateFormat
6566

6667
@OptIn(ExperimentalMaterial3Api::class)
@@ -70,10 +71,27 @@ fun CrashReportDetailScreen(navController: NavController, reportId: String) {
7071
val report = reports.find { it.id == reportId }
7172
var files by remember { mutableStateOf<List<CrashReportFile>>(emptyList()) }
7273
var isLoading by remember { mutableStateOf(true) }
73-
var shareMenuExpanded by remember { mutableStateOf(false) }
74+
var showShareDialog by remember { mutableStateOf(false) }
75+
var pendingZipFile by remember { mutableStateOf<File?>(null) }
7476
val scope = rememberCoroutineScope()
7577
val context = LocalContext.current
7678

79+
val saveLauncher = rememberLauncherForActivityResult(
80+
contract = ActivityResultContracts.CreateDocument("application/zip"),
81+
) { uri ->
82+
val zipFile = pendingZipFile
83+
pendingZipFile = null
84+
if (uri != null && zipFile != null) {
85+
scope.launch(Dispatchers.IO) {
86+
runCatching {
87+
context.contentResolver.openOutputStream(uri)?.use { output ->
88+
zipFile.inputStream().use { input -> input.copyTo(output) }
89+
}
90+
}
91+
}
92+
}
93+
}
94+
7795
LaunchedEffect(report) {
7896
if (report != null) {
7997
withContext(Dispatchers.IO) {
@@ -92,10 +110,10 @@ fun CrashReportDetailScreen(navController: NavController, reportId: String) {
92110

93111
val hasConfig = report != null && CrashReportManager.hasConfigFile(report)
94112

95-
fun shareReport(includeConfig: Boolean) {
113+
fun shareReport(includeConfig: Boolean, includeLog: Boolean) {
96114
val currentReport = report ?: return
97115
scope.launch {
98-
val zipFile = CrashReportManager.createZipArchive(currentReport, includeConfig)
116+
val zipFile = CrashReportManager.createZipArchive(currentReport, includeConfig, includeLog)
99117
val uri = FileProvider.getUriForFile(context, "${context.packageName}.cache", zipFile)
100118
val intent = Intent(Intent.ACTION_SEND).apply {
101119
type = "application/zip"
@@ -116,33 +134,8 @@ fun CrashReportDetailScreen(navController: NavController, reportId: String) {
116134
},
117135
actions = {
118136
if (!isLoading && files.isNotEmpty()) {
119-
if (hasConfig) {
120-
IconButton(onClick = { shareMenuExpanded = true }) {
121-
Icon(Icons.Default.Share, contentDescription = null)
122-
}
123-
DropdownMenu(
124-
expanded = shareMenuExpanded,
125-
onDismissRequest = { shareMenuExpanded = false },
126-
) {
127-
DropdownMenuItem(
128-
text = { Text(stringResource(R.string.report_share)) },
129-
onClick = {
130-
shareMenuExpanded = false
131-
shareReport(includeConfig = false)
132-
},
133-
)
134-
DropdownMenuItem(
135-
text = { Text(stringResource(R.string.report_share_with_config)) },
136-
onClick = {
137-
shareMenuExpanded = false
138-
shareReport(includeConfig = true)
139-
},
140-
)
141-
}
142-
} else {
143-
IconButton(onClick = { shareReport(includeConfig = false) }) {
144-
Icon(Icons.Default.Share, contentDescription = null)
145-
}
137+
IconButton(onClick = { showShareDialog = true }) {
138+
Icon(Icons.Default.Share, contentDescription = null)
146139
}
147140
IconButton(onClick = {
148141
scope.launch {
@@ -253,6 +246,25 @@ fun CrashReportDetailScreen(navController: NavController, reportId: String) {
253246
}
254247
}
255248
}
249+
250+
if (showShareDialog && report != null) {
251+
ReportShareDialog(
252+
hasConfig = hasConfig,
253+
hasLog = false,
254+
onSave = { includeConfig, includeLog ->
255+
showShareDialog = false
256+
scope.launch {
257+
pendingZipFile = CrashReportManager.createZipArchive(report, includeConfig, includeLog)
258+
saveLauncher.launch("${report.id}.zip")
259+
}
260+
},
261+
onShare = { includeConfig, includeLog ->
262+
showShareDialog = false
263+
shareReport(includeConfig, includeLog)
264+
},
265+
onDismiss = { showShareDialog = false },
266+
)
267+
}
256268
}
257269

258270
@OptIn(ExperimentalMaterial3Api::class)

app/src/main/java/io/nekohasekai/sfa/compose/screen/tools/OOMReportDetailScreen.kt

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.nekohasekai.sfa.compose.screen.tools
22

33
import android.content.Intent
4+
import androidx.activity.compose.rememberLauncherForActivityResult
5+
import androidx.activity.result.contract.ActivityResultContracts
46
import androidx.compose.foundation.background
57
import androidx.compose.foundation.clickable
68
import androidx.compose.foundation.layout.Box
@@ -16,14 +18,13 @@ import androidx.compose.material.icons.Icons
1618
import androidx.compose.material.icons.automirrored.filled.ArrowBack
1719
import androidx.compose.material.icons.filled.DataObject
1820
import androidx.compose.material.icons.filled.Delete
21+
import androidx.compose.material.icons.filled.Description
1922
import androidx.compose.material.icons.filled.Share
2023
import androidx.compose.material.icons.filled.Terminal
2124
import androidx.compose.material.icons.outlined.Settings
2225
import androidx.compose.material3.Card
2326
import androidx.compose.material3.CardDefaults
2427
import androidx.compose.material3.CircularProgressIndicator
25-
import androidx.compose.material3.DropdownMenu
26-
import androidx.compose.material3.DropdownMenuItem
2728
import androidx.compose.material3.ExperimentalMaterial3Api
2829
import androidx.compose.material3.Icon
2930
import androidx.compose.material3.IconButton
@@ -60,6 +61,7 @@ import kotlinx.coroutines.Dispatchers
6061
import kotlinx.coroutines.launch
6162
import kotlinx.coroutines.withContext
6263
import org.json.JSONObject
64+
import java.io.File
6365
import java.text.DateFormat
6466

6567
@OptIn(ExperimentalMaterial3Api::class)
@@ -69,10 +71,27 @@ fun OOMReportDetailScreen(navController: NavController, reportId: String) {
6971
val report = reports.find { it.id == reportId }
7072
var files by remember { mutableStateOf<List<OOMReportFile>>(emptyList()) }
7173
var isLoading by remember { mutableStateOf(true) }
72-
var shareMenuExpanded by remember { mutableStateOf(false) }
74+
var showShareDialog by remember { mutableStateOf(false) }
75+
var pendingZipFile by remember { mutableStateOf<File?>(null) }
7376
val scope = rememberCoroutineScope()
7477
val context = LocalContext.current
7578

79+
val saveLauncher = rememberLauncherForActivityResult(
80+
contract = ActivityResultContracts.CreateDocument("application/zip"),
81+
) { uri ->
82+
val zipFile = pendingZipFile
83+
pendingZipFile = null
84+
if (uri != null && zipFile != null) {
85+
scope.launch(Dispatchers.IO) {
86+
runCatching {
87+
context.contentResolver.openOutputStream(uri)?.use { output ->
88+
zipFile.inputStream().use { input -> input.copyTo(output) }
89+
}
90+
}
91+
}
92+
}
93+
}
94+
7695
LaunchedEffect(report) {
7796
if (report != null) {
7897
withContext(Dispatchers.IO) {
@@ -90,11 +109,12 @@ fun OOMReportDetailScreen(navController: NavController, reportId: String) {
90109
}
91110

92111
val hasConfig = report != null && OOMReportManager.hasConfigFile(report)
112+
val hasLog = report != null && OOMReportManager.hasLogFile(report)
93113

94-
fun shareReport(includeConfig: Boolean) {
114+
fun shareReport(includeConfig: Boolean, includeLog: Boolean) {
95115
val currentReport = report ?: return
96116
scope.launch {
97-
val zipFile = OOMReportManager.createZipArchive(currentReport, includeConfig)
117+
val zipFile = OOMReportManager.createZipArchive(currentReport, includeConfig, includeLog)
98118
val uri = FileProvider.getUriForFile(context, "${context.packageName}.cache", zipFile)
99119
val intent = Intent(Intent.ACTION_SEND).apply {
100120
type = "application/zip"
@@ -115,33 +135,8 @@ fun OOMReportDetailScreen(navController: NavController, reportId: String) {
115135
},
116136
actions = {
117137
if (!isLoading && files.isNotEmpty()) {
118-
if (hasConfig) {
119-
IconButton(onClick = { shareMenuExpanded = true }) {
120-
Icon(Icons.Default.Share, contentDescription = null)
121-
}
122-
DropdownMenu(
123-
expanded = shareMenuExpanded,
124-
onDismissRequest = { shareMenuExpanded = false },
125-
) {
126-
DropdownMenuItem(
127-
text = { Text(stringResource(R.string.report_share)) },
128-
onClick = {
129-
shareMenuExpanded = false
130-
shareReport(includeConfig = false)
131-
},
132-
)
133-
DropdownMenuItem(
134-
text = { Text(stringResource(R.string.report_share_with_config)) },
135-
onClick = {
136-
shareMenuExpanded = false
137-
shareReport(includeConfig = true)
138-
},
139-
)
140-
}
141-
} else {
142-
IconButton(onClick = { shareReport(includeConfig = false) }) {
143-
Icon(Icons.Default.Share, contentDescription = null)
144-
}
138+
IconButton(onClick = { showShareDialog = true }) {
139+
Icon(Icons.Default.Share, contentDescription = null)
145140
}
146141
IconButton(onClick = {
147142
scope.launch {
@@ -219,6 +214,7 @@ fun OOMReportDetailScreen(navController: NavController, reportId: String) {
219214
val icon = when (file.kind) {
220215
OOMReportFile.Kind.METADATA -> Icons.Default.DataObject
221216
OOMReportFile.Kind.CONFIG -> Icons.Outlined.Settings
217+
OOMReportFile.Kind.GO_LOG -> Icons.Default.Description
222218
OOMReportFile.Kind.PROFILE -> Icons.Default.Terminal
223219
}
224220
ListItem(
@@ -257,6 +253,25 @@ fun OOMReportDetailScreen(navController: NavController, reportId: String) {
257253
}
258254
}
259255
}
256+
257+
if (showShareDialog && report != null) {
258+
ReportShareDialog(
259+
hasConfig = hasConfig,
260+
hasLog = hasLog,
261+
onSave = { includeConfig, includeLog ->
262+
showShareDialog = false
263+
scope.launch {
264+
pendingZipFile = OOMReportManager.createZipArchive(report, includeConfig, includeLog)
265+
saveLauncher.launch("${report.id}.zip")
266+
}
267+
},
268+
onShare = { includeConfig, includeLog ->
269+
showShareDialog = false
270+
shareReport(includeConfig, includeLog)
271+
},
272+
onDismiss = { showShareDialog = false },
273+
)
274+
}
260275
}
261276

262277
@OptIn(ExperimentalMaterial3Api::class)

0 commit comments

Comments
 (0)