diff --git a/.github/workflows/nyms5-android-build.yml b/.github/workflows/nyms5-android-build.yml
index e85d9c67ad..ca8179eb16 100644
--- a/.github/workflows/nyms5-android-build.yml
+++ b/.github/workflows/nyms5-android-build.yml
@@ -68,8 +68,10 @@ jobs:
working-directory: nym-connect/native/android
env:
ANDROID_SDK_ROOT: ${{ env.ANDROID_HOME }}
+ SENTRY_AUTH_TOKEN: ${{ secrets.NYMS5_ANDROID_SENTRY_AUTH_TOKEN }}
# build for arm64 and x86_64
run: |
+ echo "auth.token=$SENTRY_AUTH_TOKEN" | tee -a sentry.properties
./gradlew :app:assembleArch64Debug
./gradlew :app:assembleArch64Release
@@ -107,4 +109,3 @@ jobs:
files: |
apk/nyms5-arch64-debug.apk
apk/nyms5-arch64-release.apk
-
diff --git a/nym-connect/native/android/app/build.gradle b/nym-connect/native/android/app/build.gradle
index 1afc11b2be..23d75605c4 100644
--- a/nym-connect/native/android/app/build.gradle
+++ b/nym-connect/native/android/app/build.gradle
@@ -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 {
@@ -101,18 +102,20 @@ dependencies {
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
- implementation 'androidx.activity:activity-compose:1.5.1'
+ implementation 'androidx.activity:activity-compose:1.7.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
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'
diff --git a/nym-connect/native/android/app/src/main/AndroidManifest.xml b/nym-connect/native/android/app/src/main/AndroidManifest.xml
index 6321a2ce8c..d013b90324 100644
--- a/nym-connect/native/android/app/src/main/AndroidManifest.xml
+++ b/nym-connect/native/android/app/src/main/AndroidManifest.xml
@@ -17,6 +17,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Nyms5"
+ android:enableOnBackInvokedCallback="true"
tools:targetApi="31">
+
+
+
+
+
+
+
+
diff --git a/nym-connect/native/android/app/src/main/java/net/nymtech/nyms5/App.kt b/nym-connect/native/android/app/src/main/java/net/nymtech/nyms5/App.kt
index 08c075a460..8a59ada991 100644
--- a/nym-connect/native/android/app/src/main/java/net/nymtech/nyms5/App.kt
+++ b/nym-connect/native/android/app/src/main/java/net/nymtech/nyms5/App.kt
@@ -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 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
diff --git a/nym-connect/native/android/app/src/main/java/net/nymtech/nyms5/MainActivity.kt b/nym-connect/native/android/app/src/main/java/net/nymtech/nyms5/MainActivity.kt
index 260a847e20..6e5a472be5 100644
--- a/nym-connect/native/android/app/src/main/java/net/nymtech/nyms5/MainActivity.kt
+++ b/nym-connect/native/android/app/src/main/java/net/nymtech/nyms5/MainActivity.kt
@@ -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,
+ 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")
+ }
+ }
+ }
+}
diff --git a/nym-connect/native/android/app/src/main/java/net/nymtech/nyms5/MainViewModel.kt b/nym-connect/native/android/app/src/main/java/net/nymtech/nyms5/MainViewModel.kt
index 3a129daef4..f100dc7676 100644
--- a/nym-connect/native/android/app/src/main/java/net/nymtech/nyms5/MainViewModel.kt
+++ b/nym-connect/native/android/app/src/main/java/net/nymtech/nyms5/MainViewModel.kt
@@ -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()
diff --git a/nym-connect/native/android/app/src/main/res/drawable/warning_24.xml b/nym-connect/native/android/app/src/main/res/drawable/warning_24.xml
new file mode 100644
index 0000000000..b1726a3610
--- /dev/null
+++ b/nym-connect/native/android/app/src/main/res/drawable/warning_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/nym-connect/native/android/app/src/main/res/values/strings.xml b/nym-connect/native/android/app/src/main/res/values/strings.xml
index 5a93a240ab..ee21a78dc9 100644
--- a/nym-connect/native/android/app/src/main/res/values/strings.xml
+++ b/nym-connect/native/android/app/src/main/res/values/strings.xml
@@ -11,4 +11,8 @@
https://harbourmaster.nymtech.net/v1/services?size=100
DpB3cHAchJiNBQi5FrZx2csXb1mrHkpYh9Wzf8Rjsuko.ANNWrvHqMYuertHGHUrZdBntQhpzfbWekB39qez9U2Vx@2BuMSfMW3zpeAjKXyKLhmY4QW1DXurrtSPEJ6CjX3SEh
Connected to the mixnet
+ 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.
+ 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.
+ You must restart the application for the change to take effect.
\ No newline at end of file
diff --git a/nym-connect/native/android/sentry.properties b/nym-connect/native/android/sentry.properties
new file mode 100644
index 0000000000..f2c03276a0
--- /dev/null
+++ b/nym-connect/native/android/sentry.properties
@@ -0,0 +1,3 @@
+defaults.project=nym-connect-android
+defaults.org=nymtech
+# auth.token=xxx