Skip to content

Commit 7b53a64

Browse files
committed
See recent logs
1 parent a23eb28 commit 7b53a64

File tree

18 files changed

+431
-58
lines changed

18 files changed

+431
-58
lines changed

composeApp/build.gradle.kts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ kotlin {
8888
iosSimulatorArm64()
8989

9090
cocoapods {
91-
ios.deploymentTarget = "12.0"
91+
ios.deploymentTarget = "15.3"
9292

9393
version = "1.0"
9494
summary = "Compose App"
@@ -100,6 +100,11 @@ kotlin {
100100
binaryOption("bundleId", "composeApp")
101101
}
102102

103+
pod("Sentry") {
104+
version = "~> 8.25"
105+
extraOpts += listOf("-compiler-option", "-fmodules")
106+
}
107+
103108
podfile = project.file("../iosApp/Podfile")
104109
}
105110

@@ -178,7 +183,11 @@ android {
178183
versionName = "1.0"
179184
resValue("string", "app_name", config.appName)
180185
resValue("string", "ooni_run_enabled", config.supportsOoniRun.toString())
181-
resValue("string", "supported_languages", config.supportedLanguages.joinToString(separator = ","))
186+
resValue(
187+
"string",
188+
"supported_languages",
189+
config.supportedLanguages.joinToString(separator = ","),
190+
)
182191
resourceConfigurations += config.supportedLanguages
183192
}
184193
packaging {

composeApp/composeApp.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ Pod::Spec.new do |spec|
88
spec.summary = 'Compose App'
99
spec.vendored_frameworks = 'build/cocoapods/framework/composeApp.framework'
1010
spec.libraries = 'c++'
11-
spec.ios.deployment_target = '12.0'
12-
11+
spec.ios.deployment_target = '15.3'
12+
spec.dependency 'Sentry', '~> 8.25'
1313

1414
if !Dir.exist?('build/cocoapods/framework/composeApp.framework') || Dir.empty?('build/cocoapods/framework/composeApp.framework')
1515
raise "

composeApp/src/commonMain/composeResources/values/strings-common.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@
100100
<string name="Settings_Advanced_RecentLogs">See recent logs</string>
101101
<string name="Settings_Advanced_LanguageSettings_Title">Language Setting</string>
102102
<string name="Settings_Storage_Label">Storage usage</string>
103+
<string name="Settings_Storage_Delete">Delete</string>
104+
<string name="Settings_Storage_Clear">Clear</string>
103105
<string name="Settings_WarmVPNInUse_Label">Warn when VPN is in use</string>
104106

105107
<string name="CategoryCode_ALDR_Name">Drugs &amp; Alcohol</string>
@@ -263,4 +265,7 @@
263265
<string name="measurement_anomaly">Anomaly</string>
264266
<string name="quiz_answer_correct">Correct answer</string>
265267
<string name="quiz_answer_incorrect">Incorrect answer</string>
268+
<string name="logs">Logs</string>
269+
<string name="share_logs">Share Logs</string>
270+
<string name="filter_logs">Filter Logs</string>
266271
</resources>

composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import co.touchlab.kermit.Logger
2222
import org.jetbrains.compose.ui.tooling.preview.Preview
2323
import org.ooni.probe.data.models.DeepLink
2424
import org.ooni.probe.di.Dependencies
25+
import org.ooni.probe.shared.PlatformInfo
2526
import org.ooni.probe.ui.navigation.BottomNavigationBar
2627
import org.ooni.probe.ui.navigation.Navigation
2728
import org.ooni.probe.ui.navigation.Screen
@@ -81,10 +82,10 @@ fun App(
8182
}
8283
}
8384

84-
Logger.addLogWriter(dependencies.crashMonitoring.logWriter)
85-
8685
LaunchedEffect(Unit) {
87-
logAppStart(dependencies)
86+
Logger.addLogWriter(dependencies.crashMonitoring.logWriter)
87+
Logger.addLogWriter(dependencies.appLogger.logWriter)
88+
logAppStart(dependencies.platformInfo)
8889
}
8990
LaunchedEffect(Unit) {
9091
dependencies.crashMonitoring.setup()
@@ -115,9 +116,9 @@ fun App(
115116
}
116117
}
117118

118-
private fun logAppStart(dependencies: Dependencies) {
119-
with(dependencies.platformInfo) {
120-
Logger.i(
119+
private fun logAppStart(platformInfo: PlatformInfo) {
120+
with(platformInfo) {
121+
Logger.v(
121122
"""
122123
---APP START---
123124
Platform: $platform ($osVersion)"

composeApp/src/commonMain/kotlin/org/ooni/probe/data/disk/ReadFile.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class ReadFileOkio(
2020
readUtf8()
2121
}
2222
} catch (e: IOException) {
23-
Logger.w("Could not read $path", e)
23+
Logger.v("Could not read $path", e)
2424
null
2525
}
2626
}

composeApp/src/commonMain/kotlin/org/ooni/probe/data/disk/WriteFile.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class WriteFileOkio(
3030
try {
3131
absolutePath.parent?.let { fileSystem.createDirectories(it) }
3232
} catch (e: IOException) {
33-
Logger.w("Could not create file $path", e)
33+
Logger.v("Could not create file $path", e)
3434
return
3535
}
3636

composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,13 @@ import org.ooni.probe.domain.ShouldShowVpnWarning
6868
import org.ooni.probe.domain.TestRunStateManager
6969
import org.ooni.probe.domain.UploadMissingMeasurements
7070
import org.ooni.probe.shared.PlatformInfo
71+
import org.ooni.probe.shared.monitoring.AppLogger
7172
import org.ooni.probe.shared.monitoring.CrashMonitoring
7273
import org.ooni.probe.ui.dashboard.DashboardViewModel
7374
import org.ooni.probe.ui.descriptor.DescriptorViewModel
7475
import org.ooni.probe.ui.descriptor.add.AddDescriptorViewModel
7576
import org.ooni.probe.ui.descriptor.review.ReviewUpdatesViewModel
77+
import org.ooni.probe.ui.log.LogViewModel
7678
import org.ooni.probe.ui.onboarding.OnboardingViewModel
7779
import org.ooni.probe.ui.result.ResultViewModel
7880
import org.ooni.probe.ui.results.ResultsViewModel
@@ -129,6 +131,14 @@ class Dependencies(
129131
// Monitoring
130132

131133
val crashMonitoring by lazy { CrashMonitoring(preferenceRepository) }
134+
val appLogger by lazy {
135+
AppLogger(
136+
readFile = readFile,
137+
writeFile = writeFile,
138+
deleteFiles = deleteFiles,
139+
backgroundDispatcher = backgroundDispatcher,
140+
)
141+
}
132142

133143
// Engine
134144

@@ -351,6 +361,13 @@ class Dependencies(
351361
descriptorUpdates = getDescriptorUpdate::observeAvailableUpdatesState,
352362
)
353363

364+
fun logViewModel(onBack: () -> Unit) =
365+
LogViewModel(
366+
onBack = onBack,
367+
readLog = appLogger::read,
368+
clearLog = appLogger::clear,
369+
)
370+
354371
fun onboardingViewModel(
355372
goToDashboard: () -> Unit,
356373
goToSettings: () -> Unit,
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package org.ooni.probe.shared.monitoring
2+
3+
import co.touchlab.kermit.LogWriter
4+
import co.touchlab.kermit.Severity
5+
import kotlinx.coroutines.CoroutineDispatcher
6+
import kotlinx.coroutines.CoroutineScope
7+
import kotlinx.coroutines.flow.Flow
8+
import kotlinx.coroutines.flow.MutableStateFlow
9+
import kotlinx.coroutines.flow.map
10+
import kotlinx.coroutines.flow.onStart
11+
import kotlinx.coroutines.flow.update
12+
import kotlinx.coroutines.launch
13+
import kotlinx.coroutines.withContext
14+
import kotlinx.datetime.LocalDateTime
15+
import okio.Path.Companion.toPath
16+
import org.ooni.probe.data.disk.DeleteFiles
17+
import org.ooni.probe.data.disk.ReadFile
18+
import org.ooni.probe.data.disk.WriteFile
19+
import org.ooni.probe.shared.now
20+
import org.ooni.probe.ui.shared.logFormat
21+
22+
class AppLogger(
23+
private val readFile: ReadFile,
24+
private val writeFile: WriteFile,
25+
private val deleteFiles: DeleteFiles,
26+
private val backgroundDispatcher: CoroutineDispatcher,
27+
) {
28+
private val log = MutableStateFlow(emptyList<String>())
29+
30+
fun read(severity: Severity?): Flow<List<String>> =
31+
log
32+
.onStart {
33+
if (log.value.isEmpty()) {
34+
log.value = readFile(FILE_PATH).orEmpty().lines()
35+
}
36+
}
37+
.map { lines ->
38+
if (severity == null) {
39+
lines
40+
} else {
41+
lines.filter { line ->
42+
line.contains(": ${severity.name.uppercase()} :")
43+
}
44+
}
45+
}
46+
47+
suspend fun clear() {
48+
withContext(backgroundDispatcher) {
49+
log.value = emptyList()
50+
deleteFiles(FILE_PATH)
51+
}
52+
}
53+
54+
val logWriter = object : LogWriter() {
55+
override fun isLoggable(
56+
tag: String,
57+
severity: Severity,
58+
): Boolean = severity != Severity.Verbose
59+
60+
override fun log(
61+
severity: Severity,
62+
message: String,
63+
tag: String,
64+
throwable: Throwable?,
65+
) {
66+
CoroutineScope(backgroundDispatcher).launch {
67+
val logMessage =
68+
"${LocalDateTime.now().logFormat()} : ${severity.name.uppercase()} : $message"
69+
log.update { lines ->
70+
val newLines = (lines + logMessage).takeLast(MAX_LINES)
71+
writeFile(FILE_PATH, newLines.joinToString("\n"), append = false)
72+
newLines
73+
}
74+
}
75+
}
76+
}
77+
78+
companion object {
79+
private val FILE_PATH = "Log/logger.txt".toPath()
80+
private const val MAX_LINES = 1000
81+
}
82+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package org.ooni.probe.ui.log
2+
3+
import androidx.compose.foundation.layout.Column
4+
import androidx.compose.foundation.layout.IntrinsicSize
5+
import androidx.compose.foundation.layout.PaddingValues
6+
import androidx.compose.foundation.layout.Row
7+
import androidx.compose.foundation.layout.WindowInsets
8+
import androidx.compose.foundation.layout.asPaddingValues
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.navigationBars
12+
import androidx.compose.foundation.layout.padding
13+
import androidx.compose.foundation.layout.width
14+
import androidx.compose.foundation.lazy.LazyColumn
15+
import androidx.compose.foundation.lazy.items
16+
import androidx.compose.material.icons.Icons
17+
import androidx.compose.material.icons.automirrored.filled.ArrowBack
18+
import androidx.compose.material3.DropdownMenuItem
19+
import androidx.compose.material3.ExposedDropdownMenuBox
20+
import androidx.compose.material3.ExposedDropdownMenuDefaults
21+
import androidx.compose.material3.Icon
22+
import androidx.compose.material3.IconButton
23+
import androidx.compose.material3.MaterialTheme
24+
import androidx.compose.material3.Text
25+
import androidx.compose.material3.TopAppBar
26+
import androidx.compose.runtime.Composable
27+
import androidx.compose.runtime.getValue
28+
import androidx.compose.runtime.mutableStateOf
29+
import androidx.compose.runtime.remember
30+
import androidx.compose.runtime.setValue
31+
import androidx.compose.ui.Alignment
32+
import androidx.compose.ui.Modifier
33+
import androidx.compose.ui.unit.dp
34+
import co.touchlab.kermit.Severity
35+
import ooniprobe.composeapp.generated.resources.Res
36+
import ooniprobe.composeapp.generated.resources.Settings_Storage_Delete
37+
import ooniprobe.composeapp.generated.resources.back
38+
import ooniprobe.composeapp.generated.resources.filter_logs
39+
import ooniprobe.composeapp.generated.resources.ic_delete_all
40+
import ooniprobe.composeapp.generated.resources.logs
41+
import org.jetbrains.compose.resources.painterResource
42+
import org.jetbrains.compose.resources.stringResource
43+
import org.ooni.probe.ui.shared.CustomFilterChip
44+
import org.ooni.probe.ui.theme.LocalCustomColors
45+
46+
@Composable
47+
fun LogScreen(
48+
state: LogViewModel.State,
49+
onEvent: (LogViewModel.Event) -> Unit,
50+
) {
51+
Column {
52+
TopAppBar(
53+
title = { Text(stringResource(Res.string.logs)) },
54+
navigationIcon = {
55+
IconButton(onClick = { onEvent(LogViewModel.Event.BackClicked) }) {
56+
Icon(
57+
Icons.AutoMirrored.Filled.ArrowBack,
58+
contentDescription = stringResource(Res.string.back),
59+
)
60+
}
61+
},
62+
actions = {
63+
IconButton(onClick = { onEvent(LogViewModel.Event.ClearClicked) }) {
64+
Icon(
65+
painterResource(Res.drawable.ic_delete_all),
66+
contentDescription = stringResource(Res.string.Settings_Storage_Delete),
67+
)
68+
}
69+
},
70+
)
71+
72+
Row(
73+
verticalAlignment = Alignment.CenterVertically,
74+
modifier = Modifier
75+
.fillMaxWidth()
76+
.padding(horizontal = 16.dp)
77+
.padding(bottom = 8.dp),
78+
) {
79+
Text(
80+
stringResource(Res.string.filter_logs),
81+
modifier = Modifier.weight(2f),
82+
)
83+
SeverityFilter(
84+
current = state.filter,
85+
onFilterChanged = { onEvent(LogViewModel.Event.FilterChanged(it)) },
86+
modifier = Modifier.weight(1f),
87+
)
88+
}
89+
90+
LazyColumn(
91+
modifier = Modifier.fillMaxSize(),
92+
contentPadding = PaddingValues(
93+
start = 16.dp,
94+
end = 16.dp,
95+
bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding(),
96+
),
97+
) {
98+
items(state.log) { line ->
99+
Text(
100+
line,
101+
style = MaterialTheme.typography.labelMedium,
102+
color = when {
103+
line.contains(": WARN : ") -> LocalCustomColors.current.logWarn
104+
line.contains(": ERROR : ") -> LocalCustomColors.current.logError
105+
line.contains(": INFO : ") -> LocalCustomColors.current.logInfo
106+
else -> LocalCustomColors.current.logDebug
107+
},
108+
)
109+
}
110+
}
111+
}
112+
}
113+
114+
@Composable
115+
fun SeverityFilter(
116+
current: Severity?,
117+
onFilterChanged: (Severity?) -> Unit,
118+
modifier: Modifier = Modifier,
119+
) {
120+
var expanded by remember { mutableStateOf(false) }
121+
122+
ExposedDropdownMenuBox(
123+
expanded = expanded,
124+
onExpandedChange = { expanded = it },
125+
modifier = modifier.width(IntrinsicSize.Min),
126+
) {
127+
CustomFilterChip(
128+
text = current.label(),
129+
selected = current != null,
130+
onClick = { expanded = true },
131+
)
132+
ExposedDropdownMenu(
133+
expanded = expanded,
134+
onDismissRequest = { expanded = false },
135+
) {
136+
SEVERITY_OPTIONS.forEach { option ->
137+
DropdownMenuItem(
138+
text = { Text(option.label()) },
139+
onClick = {
140+
onFilterChanged(option)
141+
expanded = false
142+
},
143+
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
144+
)
145+
}
146+
}
147+
}
148+
}
149+
150+
private fun Severity?.label() = this?.name?.uppercase() ?: "ALL"
151+
152+
private val SEVERITY_OPTIONS = listOf(
153+
null,
154+
Severity.Error,
155+
Severity.Warn,
156+
Severity.Info,
157+
Severity.Debug,
158+
)

0 commit comments

Comments
 (0)