Skip to content

Commit c244072

Browse files
committed
feat: CrashReportPage
1 parent 16b0c23 commit c244072

File tree

11 files changed

+302
-59
lines changed

11 files changed

+302
-59
lines changed

app/src/main/kotlin/li/songe/gkd/App.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@ import android.database.ContentObserver
1515
import android.net.Uri
1616
import android.os.PowerManager
1717
import android.provider.Settings
18+
import android.util.Log
1819
import android.view.WindowManager
1920
import android.view.accessibility.AccessibilityManager
2021
import android.view.inputmethod.InputMethodManager
2122
import androidx.core.content.ContextCompat
2223
import kotlinx.coroutines.Dispatchers
2324
import kotlinx.coroutines.MainScope
2425
import kotlinx.coroutines.delay
25-
import kotlinx.coroutines.launch
2626
import kotlinx.serialization.Serializable
2727
import li.songe.gkd.a11y.initA11yFeat
28+
import li.songe.gkd.data.CrashData
2829
import li.songe.gkd.data.selfAppInfo
2930
import li.songe.gkd.notif.initChannel
3031
import li.songe.gkd.service.clearHttpSubs
@@ -34,9 +35,11 @@ import li.songe.gkd.store.initStore
3435
import li.songe.gkd.util.AndroidTarget
3536
import li.songe.gkd.util.LogUtils
3637
import li.songe.gkd.util.PKG_FLAGS
38+
import li.songe.gkd.util.deviceInfoDesc
3739
import li.songe.gkd.util.initAppState
3840
import li.songe.gkd.util.initSubsState
3941
import li.songe.gkd.util.initToast
42+
import li.songe.gkd.util.launchTry
4043
import li.songe.gkd.util.toast
4144
import org.lsposed.hiddenapibypass.HiddenApiBypass
4245
import kotlin.system.exitProcess
@@ -196,7 +199,21 @@ class App : Application() {
196199
Thread.setDefaultUncaughtExceptionHandler { t, e ->
197200
toast(e.message ?: e.toString())
198201
LogUtils.d("UncaughtExceptionHandler", t, e)
199-
appScope.launch(Dispatchers.IO) {
202+
val mtime = System.currentTimeMillis()
203+
appScope.launchTry(Dispatchers.IO) {
204+
CrashData(
205+
id = mtime,
206+
mtime = mtime,
207+
device = deviceInfoDesc,
208+
androidVersionCode = android.os.Build.VERSION.SDK_INT,
209+
androidVersionName = android.os.Build.VERSION.RELEASE,
210+
versionCode = META.versionCode,
211+
versionName = META.versionName,
212+
name = e::class.java.name,
213+
message = e.message,
214+
thread = t.name,
215+
stackTrace = Log.getStackTraceString(e),
216+
).save()
200217
delay(1500)
201218
if (isActivityVisible) {
202219
startLaunchActivity()

app/src/main/kotlin/li/songe/gkd/MainActivity.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ import li.songe.gkd.ui.AuthA11yPage
9494
import li.songe.gkd.ui.AuthA11yRoute
9595
import li.songe.gkd.ui.BlockA11yAppListPage
9696
import li.songe.gkd.ui.BlockA11yAppListRoute
97+
import li.songe.gkd.ui.CrashReportPage
98+
import li.songe.gkd.ui.CrashReportRoute
9799
import li.songe.gkd.ui.EditBlockAppListPage
98100
import li.songe.gkd.ui.EditBlockAppListRoute
99101
import li.songe.gkd.ui.ImagePreviewPage
@@ -119,6 +121,7 @@ import li.songe.gkd.ui.WebViewRoute
119121
import li.songe.gkd.ui.component.BuildDialog
120122
import li.songe.gkd.ui.component.PerfIcon
121123
import li.songe.gkd.ui.component.ShareDataDialog
124+
import li.songe.gkd.ui.component.ShareLogDlg
122125
import li.songe.gkd.ui.component.SubsSheet
123126
import li.songe.gkd.ui.component.TermsAcceptDialog
124127
import li.songe.gkd.ui.component.TextDialog
@@ -278,6 +281,7 @@ class MainActivity : ComponentActivity() {
278281
entry<UpsertRuleGroupRoute> { UpsertRuleGroupPage(it) }
279282
entry<SubsAppGroupListRoute> { SubsAppGroupListPage(it) }
280283
entry<AppConfigRoute> { AppConfigPage(it) }
284+
entry<CrashReportRoute> { CrashReportPage() }
281285
},
282286
transitionSpec = {
283287
slideInHorizontally(initialOffsetX = { it }) togetherWith
@@ -308,6 +312,7 @@ class MainActivity : ComponentActivity() {
308312
mainVm.inputSubsLinkOption.ContentDialog()
309313
mainVm.ruleGroupState.Render()
310314
TextDialog(mainVm.textFlow)
315+
ShareLogDlg(mainVm.showShareLogDlgFlow)
311316
}
312317
}
313318
}

app/src/main/kotlin/li/songe/gkd/MainViewModel.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import kotlinx.coroutines.launch
1919
import kotlinx.coroutines.withContext
2020
import li.songe.gkd.a11y.useA11yServiceEnabledFlow
2121
import li.songe.gkd.a11y.useEnabledA11yServicesFlow
22+
import li.songe.gkd.data.CrashData
2223
import li.songe.gkd.data.RawSubscription
2324
import li.songe.gkd.data.SubsItem
2425
import li.songe.gkd.data.importData
@@ -33,6 +34,7 @@ import li.songe.gkd.store.createTextFlow
3334
import li.songe.gkd.store.storeFlow
3435
import li.songe.gkd.ui.AdvancedPageRoute
3536
import li.songe.gkd.ui.AppOpsAllowRoute
37+
import li.songe.gkd.ui.CrashReportRoute
3638
import li.songe.gkd.ui.SnapshotPageRoute
3739
import li.songe.gkd.ui.WebViewRoute
3840
import li.songe.gkd.ui.component.AlertDialogOptions
@@ -51,7 +53,10 @@ import li.songe.gkd.util.UpdateStatus
5153
import li.songe.gkd.util.appIconMapFlow
5254
import li.songe.gkd.util.clearCache
5355
import li.songe.gkd.util.client
56+
import li.songe.gkd.util.crashFolder
57+
import li.songe.gkd.util.crashTempFolder
5458
import li.songe.gkd.util.findOption
59+
import li.songe.gkd.util.json
5560
import li.songe.gkd.util.launchTry
5661
import li.songe.gkd.util.openUri
5762
import li.songe.gkd.util.openWeChatScaner
@@ -63,7 +68,9 @@ import li.songe.gkd.util.toast
6368
import li.songe.gkd.util.updateSubsMutex
6469
import li.songe.gkd.util.updateSubscription
6570
import rikka.shizuku.Shizuku
71+
import java.nio.file.Files
6672
import kotlin.reflect.jvm.jvmName
73+
import kotlin.time.Duration.Companion.days
6774

6875
class MainViewModel : BaseViewModel(), OnSimpleLife {
6976
companion object {
@@ -323,6 +330,10 @@ class MainViewModel : BaseViewModel(), OnSimpleLife {
323330
uiAutomationFlow.value?.shutdown()
324331
}
325332

333+
val showShareLogDlgFlow = MutableStateFlow(false)
334+
335+
var tempCrashDataList = emptyList<CrashData>()
336+
326337
init {
327338
// preload
328339
appIconMapFlow.value
@@ -360,6 +371,31 @@ class MainViewModel : BaseViewModel(), OnSimpleLife {
360371
// preload
361372
githubCookieFlow.value
362373
}
374+
viewModelScope.launchTry(Dispatchers.IO) {
375+
val list = (crashTempFolder.listFiles() ?: emptyArray()).mapNotNull {
376+
try {
377+
json.decodeFromString<CrashData>(it.readText())
378+
} catch (e: Exception) {
379+
LogUtils.d("解析崩溃日志失败: ${it.name}", e)
380+
null
381+
}
382+
}.sortedBy { -it.mtime }
383+
crashTempFolder.deleteRecursively()
384+
val t = System.currentTimeMillis()
385+
crashFolder.listFiles()?.filter {
386+
val name = it.name
387+
!list.any { f -> name == f.filename }
388+
}?.forEach {
389+
val mtime = Files.getLastModifiedTime(it.toPath()).toMillis()
390+
if (t - mtime > 30.days.inWholeMilliseconds) {
391+
it.delete()
392+
}
393+
}
394+
tempCrashDataList = list
395+
if (list.isNotEmpty()) {
396+
navigatePage(CrashReportRoute)
397+
}
398+
}
363399

364400
// for OnSimpleLife
365401
onCreated()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package li.songe.gkd.data
2+
3+
import kotlinx.serialization.Serializable
4+
import li.songe.gkd.util.crashFolder
5+
import li.songe.gkd.util.crashTempFolder
6+
import li.songe.gkd.util.format
7+
import li.songe.gkd.util.json
8+
9+
@Serializable
10+
data class CrashData(
11+
val id: Long,
12+
val mtime: Long,
13+
val device: String,
14+
val androidVersionCode: Int,
15+
val androidVersionName: String,
16+
val versionCode: Int,
17+
val versionName: String,
18+
val name: String,
19+
val message: String?,
20+
val thread: String,
21+
val stackTrace: String,
22+
) {
23+
val filename get() = "gkd_crash-" + mtime.format("yyyyMMdd_HHmmss") + ".json"
24+
fun save() {
25+
val text = json.encodeToString(this)
26+
crashFolder.resolve(filename).writeText(text)
27+
crashTempFolder.resolve(filename).writeText(text)
28+
}
29+
30+
}

app/src/main/kotlin/li/songe/gkd/ui/AboutPage.kt

Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ import li.songe.gkd.util.PLAY_STORE_URL
7979
import li.songe.gkd.util.REPOSITORY_URL
8080
import li.songe.gkd.util.ShortUrlSet
8181
import li.songe.gkd.util.UpdateChannelOption
82-
import li.songe.gkd.util.buildLogFile
8382
import li.songe.gkd.util.findOption
8483
import li.songe.gkd.util.format
8584
import li.songe.gkd.util.getShareApkFile
@@ -146,7 +145,6 @@ fun AboutPage() {
146145
},
147146
)
148147
}
149-
var showShareLogDlg by vm.showShareLogDlgFlow.asMutableState()
150148
var showShareAppDlg by vm.showShareAppDlgFlow.asMutableState()
151149
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
152150
Scaffold(
@@ -287,7 +285,7 @@ fun AboutPage() {
287285
title = "导出日志",
288286
imageVector = PerfIcon.Share,
289287
onClick = {
290-
showShareLogDlg = true
288+
mainVm.showShareLogDlgFlow.value = true
291289
}
292290
)
293291
if (mainVm.updateStatus != null) {
@@ -337,54 +335,6 @@ fun AboutPage() {
337335
}
338336
}
339337

340-
if (showShareLogDlg) {
341-
Dialog(onDismissRequest = { showShareLogDlg = false }) {
342-
Card(
343-
modifier = Modifier
344-
.fillMaxWidth()
345-
.padding(16.dp),
346-
shape = RoundedCornerShape(16.dp),
347-
) {
348-
val modifier = Modifier
349-
.fillMaxWidth()
350-
.padding(16.dp)
351-
Text(
352-
text = "分享到其他应用", modifier = Modifier
353-
.clickable(onClick = throttle {
354-
showShareLogDlg = false
355-
mainVm.viewModelScope.launchTry(Dispatchers.IO) {
356-
val logZipFile = buildLogFile()
357-
context.shareFile(logZipFile, "分享日志文件")
358-
}
359-
})
360-
.then(modifier)
361-
)
362-
Text(
363-
text = "保存到下载", modifier = Modifier
364-
.clickable(onClick = throttle {
365-
showShareLogDlg = false
366-
mainVm.viewModelScope.launchTry(Dispatchers.IO) {
367-
val logZipFile = buildLogFile()
368-
context.saveFileToDownloads(logZipFile)
369-
}
370-
})
371-
.then(modifier)
372-
)
373-
Text(
374-
text = "生成链接(需科学上网)",
375-
modifier = Modifier
376-
.clickable(onClick = throttle {
377-
showShareLogDlg = false
378-
mainVm.uploadOptions.startTask(
379-
getFile = { buildLogFile() }
380-
)
381-
})
382-
.then(modifier)
383-
)
384-
}
385-
}
386-
}
387-
388338
if (showShareAppDlg) {
389339
Dialog(onDismissRequest = { showShareAppDlg = false }) {
390340
Card(

app/src/main/kotlin/li/songe/gkd/ui/AboutVm.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,5 @@ import kotlinx.coroutines.flow.MutableStateFlow
55

66
class AboutVm : ViewModel() {
77
val showInfoDlgFlow = MutableStateFlow(false)
8-
val showShareLogDlgFlow = MutableStateFlow(false)
98
val showShareAppDlgFlow = MutableStateFlow(false)
109
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package li.songe.gkd.ui
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Spacer
6+
import androidx.compose.foundation.layout.fillMaxSize
7+
import androidx.compose.foundation.layout.height
8+
import androidx.compose.foundation.layout.padding
9+
import androidx.compose.foundation.layout.width
10+
import androidx.compose.foundation.verticalScroll
11+
import androidx.compose.material3.BottomAppBar
12+
import androidx.compose.material3.Scaffold
13+
import androidx.compose.material3.Text
14+
import androidx.compose.material3.TextButton
15+
import androidx.compose.runtime.Composable
16+
import androidx.compose.runtime.mutableIntStateOf
17+
import androidx.compose.runtime.saveable.rememberSaveable
18+
import androidx.compose.ui.Modifier
19+
import androidx.compose.ui.input.nestedscroll.nestedScroll
20+
import androidx.compose.ui.unit.dp
21+
import androidx.lifecycle.viewmodel.compose.viewModel
22+
import androidx.navigation3.runtime.NavKey
23+
import kotlinx.serialization.Serializable
24+
import li.songe.gkd.ui.component.CopyTextCard
25+
import li.songe.gkd.ui.component.EmptyText
26+
import li.songe.gkd.ui.component.PerfIcon
27+
import li.songe.gkd.ui.component.PerfIconButton
28+
import li.songe.gkd.ui.component.PerfTopAppBar
29+
import li.songe.gkd.ui.component.useScrollBehaviorState
30+
import li.songe.gkd.ui.share.LocalMainViewModel
31+
import li.songe.gkd.ui.share.noRippleClickable
32+
import li.songe.gkd.ui.style.EmptyHeight
33+
import li.songe.gkd.ui.style.itemHorizontalPadding
34+
import li.songe.gkd.ui.style.itemVerticalPadding
35+
import li.songe.gkd.util.ISSUES_URL
36+
import li.songe.gkd.util.throttle
37+
38+
39+
@Serializable
40+
data object CrashReportRoute : NavKey
41+
42+
@Composable
43+
fun CrashReportPage() {
44+
val mainVm = LocalMainViewModel.current
45+
val vm = viewModel<CrashReportVm>()
46+
val scrollKey = rememberSaveable { mutableIntStateOf(0) }
47+
val (scrollBehavior, scrollState) = useScrollBehaviorState(scrollKey)
48+
Scaffold(
49+
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
50+
topBar = {
51+
PerfTopAppBar(
52+
scrollBehavior = scrollBehavior,
53+
navigationIcon = {
54+
PerfIconButton(
55+
imageVector = PerfIcon.ArrowBack,
56+
onClick = mainVm::popPage,
57+
)
58+
},
59+
title = {
60+
Text(
61+
text = "崩溃记录",
62+
modifier = Modifier.noRippleClickable(onClick = throttle { scrollKey.intValue++ })
63+
)
64+
},
65+
)
66+
},
67+
bottomBar = {
68+
if (vm.crashDataList.isNotEmpty()) {
69+
BottomAppBar {
70+
Spacer(modifier = Modifier.weight(1f))
71+
TextButton(
72+
onClick = throttle { mainVm.openUrl(ISSUES_URL) },
73+
) {
74+
Text(text = "问题反馈")
75+
}
76+
Spacer(modifier = Modifier.width(itemHorizontalPadding))
77+
TextButton(
78+
onClick = { mainVm.showShareLogDlgFlow.value = true },
79+
) {
80+
Text(text = "导出日志")
81+
}
82+
Spacer(modifier = Modifier.width(itemHorizontalPadding))
83+
}
84+
}
85+
},
86+
) { contentPadding ->
87+
Column(
88+
modifier = Modifier
89+
.verticalScroll(scrollState)
90+
.fillMaxSize()
91+
.padding(contentPadding),
92+
verticalArrangement = Arrangement.spacedBy(itemVerticalPadding)
93+
) {
94+
if (vm.crashDataList.isNotEmpty()) {
95+
vm.crashDataList.forEach { crashData ->
96+
CopyTextCard(
97+
text = crashData.stackTrace,
98+
modifier = Modifier.padding(horizontal = 8.dp),
99+
)
100+
}
101+
} else {
102+
Spacer(modifier = Modifier.height(EmptyHeight))
103+
EmptyText()
104+
}
105+
Spacer(modifier = Modifier.height(EmptyHeight))
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)