Skip to content
This repository was archived by the owner on Jul 2, 2024. It is now read-only.

Commit 6b9a8c2

Browse files
committed
Add Logcat
1 parent 2b5f39f commit 6b9a8c2

File tree

3 files changed

+166
-2
lines changed

3 files changed

+166
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.sanmer.mrepo.app
2+
3+
import android.content.Context
4+
import android.net.Uri
5+
import kotlinx.coroutines.Dispatchers
6+
import kotlinx.coroutines.withContext
7+
import timber.log.Timber
8+
import java.time.LocalDateTime
9+
10+
object Logcat {
11+
val logfile get() = "MRepo_${LocalDateTime.now()}.log"
12+
13+
private suspend fun dump(context: Context) = withContext(Dispatchers.IO) {
14+
val uid = context.applicationInfo.uid
15+
val command = arrayOf(
16+
"logcat",
17+
"-d",
18+
"--uid=${uid}"
19+
)
20+
21+
try {
22+
val process = Runtime.getRuntime().exec(command)
23+
24+
val result = process.inputStream.use { stream ->
25+
stream.reader().readLines()
26+
.filterNot { it.startsWith("------") }
27+
.joinToString("\n")
28+
}
29+
30+
process.waitFor()
31+
32+
result.trim()
33+
} catch (e: Exception) {
34+
""
35+
}
36+
}
37+
38+
suspend fun write(
39+
context: Context,
40+
writer: (ByteArray) -> Unit
41+
) = withContext(Dispatchers.IO) {
42+
val logs = dump(context)
43+
writer(logs.toByteArray())
44+
}
45+
46+
suspend fun writeTo(context: Context, uri: Uri) = withContext(Dispatchers.IO) {
47+
runCatching {
48+
val cr = context.contentResolver
49+
cr.openOutputStream(uri)?.use {
50+
write(context, it::write)
51+
}
52+
}.onFailure {
53+
Timber.d(it)
54+
}
55+
}
56+
}

app/src/main/kotlin/com/sanmer/mrepo/ui/screens/settings/SettingsScreen.kt

+50-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.sanmer.mrepo.ui.screens.settings
22

3+
import androidx.activity.compose.rememberLauncherForActivityResult
4+
import androidx.activity.result.contract.ActivityResultContracts
35
import androidx.compose.foundation.layout.Column
46
import androidx.compose.foundation.layout.WindowInsets
57
import androidx.compose.foundation.layout.fillMaxWidth
@@ -9,22 +11,28 @@ import androidx.compose.foundation.verticalScroll
911
import androidx.compose.material3.Icon
1012
import androidx.compose.material3.IconButton
1113
import androidx.compose.material3.Scaffold
14+
import androidx.compose.material3.SnackbarDuration
15+
import androidx.compose.material3.SnackbarHost
16+
import androidx.compose.material3.SnackbarHostState
1217
import androidx.compose.material3.TopAppBar
1318
import androidx.compose.material3.TopAppBarDefaults
1419
import androidx.compose.material3.TopAppBarScrollBehavior
1520
import androidx.compose.runtime.Composable
1621
import androidx.compose.runtime.getValue
1722
import androidx.compose.runtime.mutableStateOf
1823
import androidx.compose.runtime.remember
24+
import androidx.compose.runtime.rememberCoroutineScope
1925
import androidx.compose.runtime.setValue
2026
import androidx.compose.ui.Modifier
2127
import androidx.compose.ui.input.nestedscroll.nestedScroll
28+
import androidx.compose.ui.platform.LocalContext
2229
import androidx.compose.ui.res.painterResource
2330
import androidx.compose.ui.res.stringResource
2431
import androidx.hilt.navigation.compose.hiltViewModel
2532
import androidx.navigation.NavController
2633
import com.sanmer.mrepo.BuildConfig
2734
import com.sanmer.mrepo.R
35+
import com.sanmer.mrepo.app.Logcat
2836
import com.sanmer.mrepo.datastore.WorkingMode
2937
import com.sanmer.mrepo.ui.component.SettingNormalItem
3038
import com.sanmer.mrepo.ui.component.TopAppBarTitle
@@ -35,24 +43,55 @@ import com.sanmer.mrepo.ui.screens.settings.items.RootItem
3543
import com.sanmer.mrepo.ui.utils.navigateSingleTopTo
3644
import com.sanmer.mrepo.ui.utils.none
3745
import com.sanmer.mrepo.viewmodel.SettingsViewModel
46+
import kotlinx.coroutines.launch
3847

3948
@Composable
4049
fun SettingsScreen(
4150
navController: NavController,
4251
viewModel: SettingsViewModel = hiltViewModel()
4352
) {
4453
val userPreferences = LocalUserPreferences.current
45-
4654
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
55+
val scope = rememberCoroutineScope()
56+
val snackbarHostState = remember { SnackbarHostState() }
57+
58+
val context = LocalContext.current
59+
val launcher = rememberLauncherForActivityResult(
60+
ActivityResultContracts.CreateDocument("*/*")
61+
) { uri ->
62+
if (uri == null) return@rememberLauncherForActivityResult
63+
64+
scope.launch {
65+
Logcat.writeTo(context, uri)
66+
.onSuccess {
67+
val message = context.getString(R.string.install_logs_saved)
68+
snackbarHostState.showSnackbar(
69+
message = message,
70+
duration = SnackbarDuration.Short
71+
)
72+
}.onFailure {
73+
val message = context.getString(
74+
R.string.install_logs_save_failed,
75+
it.message ?: context.getString(R.string.unknown_error)
76+
)
77+
snackbarHostState.showSnackbar(
78+
message = message,
79+
duration = SnackbarDuration.Short
80+
)
81+
}
82+
}
83+
}
4784

4885
Scaffold(
4986
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
5087
topBar = {
5188
TopBar(
89+
exportLog = { launcher.launch(Logcat.logfile) },
5290
isRoot = userPreferences.isRoot,
5391
scrollBehavior = scrollBehavior
5492
)
5593
},
94+
snackbarHost = { SnackbarHost(snackbarHostState) },
5695
contentWindowInsets = WindowInsets.none
5796
) { innerPadding ->
5897
Column(
@@ -115,15 +154,24 @@ fun SettingsScreen(
115154

116155
@Composable
117156
private fun TopBar(
157+
exportLog: () -> Unit,
118158
isRoot: Boolean,
119159
scrollBehavior: TopAppBarScrollBehavior
120160
) = TopAppBar(
121161
title = {
122162
TopAppBarTitle(text = stringResource(id = R.string.page_settings))
123163
},
124164
actions = {
125-
var expanded by remember { mutableStateOf(false) }
165+
IconButton(
166+
onClick = exportLog
167+
) {
168+
Icon(
169+
painter = painterResource(id = R.drawable.bug),
170+
contentDescription = null
171+
)
172+
}
126173

174+
var expanded by remember { mutableStateOf(false) }
127175
IconButton(
128176
onClick = { expanded = true },
129177
enabled = isRoot

app/src/main/res/drawable/bug.xml

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:pathData="M9,9v-1a3,3 0,0 1,6 0v1"
8+
android:strokeLineJoin="round"
9+
android:strokeWidth="2"
10+
android:strokeColor="#ffff"
11+
android:strokeLineCap="round"/>
12+
<path
13+
android:pathData="M8,9h8a6,6 0,0 1,1 3v3a5,5 0,0 1,-10 0v-3a6,6 0,0 1,1 -3"
14+
android:strokeLineJoin="round"
15+
android:strokeWidth="2"
16+
android:strokeColor="#ffff"
17+
android:strokeLineCap="round"/>
18+
<path
19+
android:pathData="M3,13l4,0"
20+
android:strokeLineJoin="round"
21+
android:strokeWidth="2"
22+
android:strokeColor="#ffff"
23+
android:strokeLineCap="round"/>
24+
<path
25+
android:pathData="M17,13l4,0"
26+
android:strokeLineJoin="round"
27+
android:strokeWidth="2"
28+
android:strokeColor="#ffff"
29+
android:strokeLineCap="round"/>
30+
<path
31+
android:pathData="M12,20l0,-6"
32+
android:strokeLineJoin="round"
33+
android:strokeWidth="2"
34+
android:strokeColor="#ffff"
35+
android:strokeLineCap="round"/>
36+
<path
37+
android:pathData="M4,19l3.35,-2"
38+
android:strokeLineJoin="round"
39+
android:strokeWidth="2"
40+
android:strokeColor="#ffff"
41+
android:strokeLineCap="round"/>
42+
<path
43+
android:pathData="M20,19l-3.35,-2"
44+
android:strokeLineJoin="round"
45+
android:strokeWidth="2"
46+
android:strokeColor="#ffff"
47+
android:strokeLineCap="round"/>
48+
<path
49+
android:pathData="M4,7l3.75,2.4"
50+
android:strokeLineJoin="round"
51+
android:strokeWidth="2"
52+
android:strokeColor="#ffff"
53+
android:strokeLineCap="round"/>
54+
<path
55+
android:pathData="M20,7l-3.75,2.4"
56+
android:strokeLineJoin="round"
57+
android:strokeWidth="2"
58+
android:strokeColor="#ffff"
59+
android:strokeLineCap="round"/>
60+
</vector>

0 commit comments

Comments
 (0)