Compare commits

...

1 Commits

Author SHA1 Message Date
pierre 81e133b789 feat(nc-android): sentry integration and topbar navigation 2023-06-22 14:21:24 +02:00
7 changed files with 228 additions and 12 deletions
@@ -2,6 +2,7 @@ plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.21'
id "io.sentry.android.gradle" version "3.11.0"
}
android {
@@ -107,12 +108,14 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation platform('androidx.compose:compose-bom:2022.10.00')
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1'
implementation 'androidx.navigation:navigation-compose:2.6.0'
implementation 'androidx.compose.runtime:runtime-livedata'
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.work:work-runtime-ktx:2.8.1'
implementation 'androidx.datastore:datastore-preferences:1.0.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@@ -17,6 +17,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Nyms5"
android:enableOnBackInvokedCallback="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
@@ -34,6 +35,20 @@
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
<!-- Sentry -->
<meta-data
android:name="io.sentry.auto-init"
android:value="false" />
<!-- enable screenshot for crashes -->
<meta-data
android:name="io.sentry.attach-screenshot"
android:value="true" />
<!-- enable view hierarchy for crashes -->
<meta-data
android:name="io.sentry.attach-view-hierarchy"
android:value="true" />
</application>
</manifest>
@@ -1,9 +1,21 @@
package net.nymtech.nyms5
import android.app.Application
import android.content.Context
import android.util.Log
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import androidx.work.Configuration
import androidx.work.DelegatingWorkerFactory
import io.sentry.android.core.SentryAndroid
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
val monitoringKey = booleanPreferencesKey("monitoring")
class App : Application(), Configuration.Provider {
companion object {
@@ -13,6 +25,30 @@ class App : Application(), Configuration.Provider {
private val tag = "App"
override fun onCreate() {
super.onCreate()
val app = this
runBlocking {
val monitoring = applicationContext.dataStore.data.map { preferences ->
preferences[monitoringKey] ?: false
}.first()
if (monitoring) {
Log.i(tag, "Performance monitoring and error reporting enabled")
SentryAndroid.init(app) { options ->
options.dsn =
"https://6872f5818bc147ef9c0fce114fcaac8a@o967446.ingest.sentry.io/4505306218102784"
options.enableAllAutoBreadcrumbs(true)
// TODO should be adjusted in production env
options.tracesSampleRate = 1.0
options.profilesSampleRate = 1.0
}
}
}
}
override fun getWorkManagerConfiguration(): Configuration {
val workerFactory = DelegatingWorkerFactory()
// pass in the NymProxy class instance
@@ -14,11 +14,21 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.CenterAlignedTopAppBar
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.Switch
import androidx.compose.material3.Text
@@ -34,6 +44,8 @@ import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import net.nymtech.nyms5.ui.theme.NymTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
@@ -42,8 +54,17 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.work.WorkInfo
import androidx.work.WorkManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.map
class MainActivity : ComponentActivity() {
private val tag = "MainActivity"
@@ -81,18 +102,16 @@ class MainActivity : ComponentActivity() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
Log.d(tag, "____uiState collect")
viewModel.uiState.collect {
setContent {
NymTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Log.d(tag, "____UI recompose")
applicationContext.dataStore.data.map { preferences ->
preferences[monitoringKey] ?: false
}.collect { monitoring ->
viewModel.uiState.collect {
setContent {
NymTheme {
val loading = it.loading
S5ClientSwitch(it.connected, loading, {
HomeScreen(it, monitoring, applicationContext.dataStore) {
if (!loading) {
when {
it -> {
@@ -106,7 +125,7 @@ class MainActivity : ComponentActivity() {
}
}
}
})
}
}
}
}
@@ -121,6 +140,85 @@ class MainActivity : ComponentActivity() {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
proxyState: MainViewModel.ProxyState,
monitoring: Boolean,
dataStore: DataStore<Preferences>,
onSwitch: (value: Boolean) -> Unit,
) {
val navController = rememberNavController()
var expanded by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
Scaffold(topBar = {
CenterAlignedTopAppBar(
title = {
Text(stringResource(R.string.app_name))
},
navigationIcon = {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
if (currentRoute === "proxy") {
IconButton(onClick = { expanded = true }) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = "Main menu"
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(onClick = {
navController.navigate("monitoring") {
popUpTo("proxy")
}
expanded = false
}, text = {
Text("Error reporting")
})
}
} else {
IconButton(onClick = {
navController.navigate("proxy") {
popUpTo("proxy")
}
}) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back home"
)
}
}
},
)
}) { contentPadding ->
NavHost(
navController = navController,
startDestination = "proxy",
modifier = Modifier.padding(contentPadding)
) {
composable("proxy") {
S5ClientSwitch(
connected = proxyState.connected,
loading = proxyState.loading,
onSwitch = onSwitch
)
}
composable("monitoring") {
Monitoring(initialValue = monitoring) {
scope.launch(Dispatchers.IO) {
dataStore.edit { settings -> settings[monitoringKey] = it }
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun S5ClientSwitch(
@@ -203,6 +301,46 @@ fun S5ClientSwitch(
}
}
@Composable
fun Monitoring(
modifier: Modifier = Modifier,
initialValue: Boolean,
onSwitch: (value: Boolean) -> Unit,
) {
var monitoring by remember { mutableStateOf(initialValue) }
Column(
modifier = modifier
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text("Enable error reporting")
Spacer(modifier = modifier.width(16.dp))
Switch(checked = monitoring, onCheckedChange = {
monitoring = it
onSwitch(it)
})
}
Spacer(modifier = modifier.height(18.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
painter = painterResource(R.drawable.warning_24),
contentDescription = "copy to clipboard",
tint = Color.Yellow
)
Spacer(modifier = modifier.width(16.dp))
Text(stringResource(R.string.monitoring_desc_3), color = Color.Yellow)
}
Spacer(modifier = modifier.height(18.dp))
Text(stringResource(R.string.monitoring_desc_1))
Spacer(modifier = modifier.height(18.dp))
Text(stringResource(R.string.monitoring_desc_2))
}
}
@Preview
@Composable
fun PreviewSocks5Client() {
@@ -225,3 +363,18 @@ fun PreviewSocks5Client() {
}
}
}
@Preview
@Composable
fun PreviewMonitoring() {
NymTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Monitoring(initialValue = false) {
Log.d("Monitoring", "switch $it")
}
}
}
}
@@ -27,7 +27,7 @@ class MainViewModel(
private val workManager: WorkManager,
private val nymProxy: NymProxy
) : ViewModel() {
private val tag = "viewModel"
private val tag = "MainViewModel"
private val workRequest: OneTimeWorkRequest =
OneTimeWorkRequestBuilder<ProxyWorker>()
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>
@@ -11,4 +11,8 @@
<string name="sp_url">https://harbourmaster.nymtech.net/v1/services?size=100</string>
<string name="default_sp">DpB3cHAchJiNBQi5FrZx2csXb1mrHkpYh9Wzf8Rjsuko.ANNWrvHqMYuertHGHUrZdBntQhpzfbWekB39qez9U2Vx@2BuMSfMW3zpeAjKXyKLhmY4QW1DXurrtSPEJ6CjX3SEh</string>
<string name="connected_text">Connected to the mixnet</string>
<string name="monitoring_desc_1">Help Nym developers to fix errors, crashes and improve the application by enabling this option. If errors occur or if the app crashes, it will automatically send a report. Also it tracks various performance metrics. We use sentry.io service to handle this.</string>
<string name="monitoring_desc_2">Note: A report can include your external IP, this can be useful to catch issues related to IP location.
All recorded data is used by Nym developers and for app development purposes only.</string>
<string name="monitoring_desc_3">You must restart the application for the change to take effect.</string>
</resources>