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

Commit

Permalink
feat: logs export
Browse files Browse the repository at this point in the history
Added export to file logs
Added copy individual log message to clipboard
Added clear logs feature

Rename account to credentials
Remove fastest country

Bump to latest lib version
  • Loading branch information
zaneschepke committed Mar 9, 2024
1 parent 0beac70 commit e5b2ebc
Show file tree
Hide file tree
Showing 30 changed files with 255 additions and 100 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.material.icons.extended)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
Expand Down
3 changes: 0 additions & 3 deletions app/src/main/java/net/nymtech/nymvpn/NymVPN.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import dagger.hilt.android.HiltAndroidApp
import net.nymtech.nymvpn.service.tile.QuickTile
import net.nymtech.nymvpn.util.Constants
import net.nymtech.nymvpn.util.log.DebugTree
import net.nymtech.nymvpn.util.log.ReleaseTree
import net.nymtech.nymvpn.util.navigationBarHeight
Expand All @@ -21,8 +20,6 @@ class NymVPN : Application() {
super.onCreate()
instance = this
if (BuildConfig.DEBUG) Timber.plant(DebugTree()) else Timber.plant(ReleaseTree())
//set lib env vars
Constants.setupEnvironment()
}

companion object {
Expand Down
33 changes: 26 additions & 7 deletions app/src/main/java/net/nymtech/nymvpn/ui/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import android.app.Application
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
Expand All @@ -26,11 +26,14 @@ import net.nymtech.nymvpn.model.Country
import net.nymtech.nymvpn.service.gateway.GatewayApiService
import net.nymtech.nymvpn.ui.theme.Theme
import net.nymtech.nymvpn.util.Constants
import net.nymtech.nymvpn.util.FileUtils
import net.nymtech.nymvpn.util.log.NymLibException
import timber.log.Timber
import java.time.Instant
import java.util.Locale
import javax.inject.Inject


@HiltViewModel
class AppViewModel @Inject constructor(
private val dataStoreManager: DataStoreManager,
Expand All @@ -53,7 +56,7 @@ class AppViewModel @Inject constructor(
AppUiState()
)

fun logCatOutput() = viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
fun readLogCatOutput() = viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
launch {
LogcatHelper.logs {
logsBuffer.add(it)
Expand All @@ -77,6 +80,19 @@ class AppViewModel @Inject constructor(
} while (true)
}
}

fun clearLogs() {
logs.clear()
logsBuffer.clear()
LogcatHelper.clear()
}

fun saveLogsToFile() {
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
val content = logs.joinToString(separator = "\n")
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
showSnackbarMessage(application.getString(R.string.logs_saved))
}
fun updateCountryListCache() {
viewModelScope.launch(Dispatchers.IO) {
try {
Expand Down Expand Up @@ -114,11 +130,10 @@ class AppViewModel @Inject constructor(
putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject))
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
ContextCompat.startActivity(
application,
Intent.createChooser(intent, application.getString(R.string.email_chooser)),
null,
)
application.startActivity(
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
} catch (e: ActivityNotFoundException) {
Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_email_detected))
Expand All @@ -138,4 +153,8 @@ class AppViewModel @Inject constructor(
snackbarMessageConsumed = true
)
}

fun showFeatureInProgressMessage() {
Toast.makeText(application.applicationContext, application.getString(R.string.feature_in_progress), Toast.LENGTH_LONG).show()
}
}
6 changes: 3 additions & 3 deletions app/src/main/java/net/nymtech/nymvpn/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class MainActivity : ComponentActivity() {

LaunchedEffect(Unit) {
appViewModel.updateCountryListCache()
appViewModel.logCatOutput()
appViewModel.readLogCatOutput()
requestNotificationPermission()
}

Expand Down Expand Up @@ -130,7 +130,7 @@ class MainActivity : ComponentActivity() {
// A surface container using the 'background' color from the theme
TransparentSystemBars()
Scaffold(
topBar = { NavBar(navController) },
topBar = { NavBar(appViewModel,navController) },
snackbarHost = {
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
CustomSnackBar(message = snackbarData.visuals.message)
Expand All @@ -155,7 +155,7 @@ class MainActivity : ComponentActivity() {
composable(NavItem.Settings.Feedback.route) { FeedbackScreen(appViewModel) }
composable(NavItem.Settings.Legal.route) { LegalScreen(appViewModel,navController) }
composable(NavItem.Settings.Login.route) { LoginScreen(navController, appViewModel) }
composable(NavItem.Settings.Account.route) { AccountScreen() }
composable(NavItem.Settings.Account.route) { AccountScreen(appViewModel) }
composable(NavItem.Settings.Legal.Licenses.route){ LicensesScreen(appViewModel) }
}
}
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/java/net/nymtech/nymvpn/ui/Navigation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package net.nymtech.nymvpn.ui

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.outlined.DeleteForever
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.ui.graphics.vector.ImageVector
import net.nymtech.nymvpn.R
Expand Down Expand Up @@ -36,14 +37,14 @@ sealed class NavItem(val route: String, val title : StringValue, val leading : I
data object Main : NavItem(Screen.MAIN.name, StringValue.StringResource(R.string.app_name), null, settingsIcon)
data object Settings : NavItem(Screen.SETTINGS.name, StringValue.StringResource(R.string.settings), backIcon) {
data object Display : NavItem("${Screen.SETTINGS.name}/${Screen.DISPLAY.name}", StringValue.StringResource(R.string.display_theme), backIcon)
data object Logs : NavItem("${Screen.SETTINGS.name}/${Screen.LOGS.name}", StringValue.StringResource(R.string.logs), backIcon)
data object Logs : NavItem("${Screen.SETTINGS.name}/${Screen.LOGS.name}", StringValue.StringResource(R.string.logs), backIcon, trailing = clearLogsIcon)
data object Feedback : NavItem("${Screen.SETTINGS.name}/${Screen.FEEDBACK.name}", StringValue.StringResource(R.string.feedback), backIcon)
data object Support : NavItem("${Screen.SETTINGS.name}/${Screen.SUPPORT.name}", StringValue.StringResource(R.string.support), backIcon)
data object Legal : NavItem("${Screen.SETTINGS.name}/${Screen.LEGAL.name}", StringValue.StringResource(R.string.legal), backIcon) {
data object Licenses : NavItem("${Screen.SETTINGS.name}/${Screen.LEGAL.name}/${Screen.LICENSES.name}", StringValue.StringResource(R.string.licenses), backIcon)
}
data object Login : NavItem("${Screen.SETTINGS.name}/${Screen.LOGIN.name}", StringValue.StringResource(R.string.login), backIcon)
data object Account : NavItem("${Screen.SETTINGS.name}/${Screen.ACCOUNT.name}", StringValue.StringResource(R.string.account), backIcon)
data object Account : NavItem("${Screen.SETTINGS.name}/${Screen.ACCOUNT.name}", StringValue.StringResource(R.string.credential), backIcon)
}
sealed class Hop {
data object Entry : NavItem("${Screen.HOP.name}/${HopType.FIRST.name}", HopType.FIRST.hopTitle(), backIcon)
Expand All @@ -53,6 +54,7 @@ sealed class NavItem(val route: String, val title : StringValue, val leading : I
companion object {
val settingsIcon = Icons.Outlined.Settings
val backIcon = Icons.AutoMirrored.Filled.ArrowBack
val clearLogsIcon = Icons.Outlined.DeleteForever
fun from(route: String?) : NavItem {
return when (route) {
Main.route -> Main
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import net.nymtech.nymvpn.ui.AppViewModel
import net.nymtech.nymvpn.ui.NavItem
import net.nymtech.nymvpn.ui.theme.iconSize

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NavBar(navController: NavController) {
fun NavBar(appViewModel: AppViewModel, navController: NavController) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val navItem = NavItem.from(navBackStackEntry?.destination?.route)
val context = LocalContext.current
Expand All @@ -32,9 +33,10 @@ fun NavBar(navController: NavController) {
navItem.trailing?.let {
IconButton(
onClick = {
when {
it == NavItem.settingsIcon -> navController.navigate(NavItem.Settings.route)
}
when (it) {
NavItem.settingsIcon -> navController.navigate(NavItem.Settings.route)
NavItem.clearLogsIcon -> appViewModel.clearLogs()
}
}) {
Icon(imageVector = it, contentDescription = it.name, tint = MaterialTheme.colorScheme.onSurface, modifier = Modifier.size(
iconSize))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ fun SettingsScreen(navController: NavController, appUiState: AppUiState, viewMod
SelectionItem(
Icons.Filled.AccountCircle,
onClick = { navController.navigate(NavItem.Settings.Account.route) },
title = stringResource(R.string.account),
title = stringResource(R.string.credential),
description = accountDescription.text)
))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class SettingsViewModel @Inject constructor(

private suspend fun setFirstHopToDefault() {
//TODO how we determine default will change
dataStoreManager.saveToDataStore(DataStoreManager.FIRST_HOP_COUNTRY_ISO, Country(isFastest = true).toString())
dataStoreManager.saveToDataStore(DataStoreManager.FIRST_HOP_COUNTRY_ISO, Country().toString())
}

fun onAutoConnectSelected(selected: Boolean) = viewModelScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import net.nymtech.nymvpn.R
import net.nymtech.nymvpn.ui.AppViewModel
import net.nymtech.nymvpn.ui.common.buttons.MainStyledButton
import net.nymtech.nymvpn.ui.common.buttons.surface.SelectionItem
import net.nymtech.nymvpn.ui.common.buttons.surface.SurfaceSelectionGroupButton
import net.nymtech.nymvpn.ui.common.labels.GroupLabel
import net.nymtech.nymvpn.util.scaledHeight

@Composable
fun AccountScreen(viewModel: AccountViewModel = hiltViewModel()) {
fun AccountScreen(appViewModel: AppViewModel, viewModel: AccountViewModel = hiltViewModel()) {

val context = LocalContext.current

Expand Down Expand Up @@ -91,14 +92,14 @@ fun AccountScreen(viewModel: AccountViewModel = hiltViewModel()) {
.fillMaxWidth()
) {
Text(
stringResource(id = R.string.top_up_account),
stringResource(id = R.string.top_up_credential),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(end = 24.dp)
)
Box(modifier = Modifier.width(91.dp)) {
MainStyledButton(
onClick = { /*TODO handle top up*/ },
onClick = { appViewModel.showFeatureInProgressMessage() },
content = {
Text(
stringResource(id = R.string.top_up),
Expand All @@ -121,7 +122,9 @@ fun AccountScreen(viewModel: AccountViewModel = hiltViewModel()) {
.fillMaxWidth()
) {
GroupLabel(title = stringResource(R.string.devices))
IconButton(onClick = { /*TODO*/ }, modifier = Modifier.padding(start = 24.dp)) {
IconButton(onClick = {
appViewModel.showFeatureInProgressMessage()
}, modifier = Modifier.padding(start = 24.dp)) {
Icon(Icons.Filled.Add, Icons.Filled.Add.name, tint = MaterialTheme.colorScheme.onSurface)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import net.nymtech.nymvpn.data.datastore.DataStoreManager
import net.nymtech.nymvpn.ui.screens.settings.account.model.Device
import net.nymtech.nymvpn.ui.screens.settings.account.model.DeviceType
import net.nymtech.nymvpn.util.Constants
import javax.inject.Inject

Expand All @@ -18,11 +16,11 @@ class AccountViewModel @Inject constructor(
) : ViewModel() {

val uiState = dataStoreManager.preferencesFlow.map {
//TODO mock for now
//TODO mocked for now
AccountUiState(
loading = false,
devices = listOf(Device("Sparrow", DeviceType.MAC_OS), Device("Falcon 1", DeviceType.ANDROID)),
subscriptionDaysRemaining = 21,
devices = emptyList(),
subscriptionDaysRemaining = 31,
subscriptionTotalDays = 31
)
}.stateIn(viewModelScope,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.nymtech.nymvpn.ui.screens.settings.feedback

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
Expand All @@ -8,9 +9,15 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
Expand All @@ -24,15 +31,42 @@ import net.nymtech.nymvpn.R
import net.nymtech.nymvpn.ui.AppViewModel
import net.nymtech.nymvpn.ui.common.buttons.surface.SelectionItem
import net.nymtech.nymvpn.ui.common.buttons.surface.SurfaceSelectionGroupButton
import net.nymtech.nymvpn.ui.theme.CustomColors
import net.nymtech.nymvpn.util.scaledHeight
import net.nymtech.nymvpn.util.scaledWidth

@Composable
fun FeedbackScreen(appViewModel: AppViewModel, viewModel: FeedbackViewModel = hiltViewModel()) {

val isErrorReportingEnabled by viewModel.isErrorReportingEnabled.collectAsStateWithLifecycle()
var showErrorReportingDialog by remember { mutableStateOf(false) }

val context = LocalContext.current

val context = LocalContext.current

AnimatedVisibility(showErrorReportingDialog) {
AlertDialog(
containerColor = CustomColors.snackBarBackgroundColor,
onDismissRequest = { showErrorReportingDialog = false },
confirmButton = {
TextButton(
onClick = {
showErrorReportingDialog = false
viewModel.onErrorReportingSelected(!isErrorReportingEnabled)
},
) {
Text(text = stringResource(R.string.okay))
}
},
dismissButton = {
TextButton(onClick = { showErrorReportingDialog = false }) {
Text(text = stringResource(R.string.cancel))
}
},
title = { Text(text = stringResource(R.string.error_reporting), color = CustomColors.snackbarTextColor) },
text = { Text(text = stringResource(R.string.error_reporting_alert), color = CustomColors.snackbarTextColor) },
)
}

Column(
horizontalAlignment = Alignment.Start,
Expand Down Expand Up @@ -83,7 +117,7 @@ fun FeedbackScreen(appViewModel: AppViewModel, viewModel: FeedbackViewModel = hi
title = stringResource(R.string.error_reporting),
description = stringResource(R.string.error_reporting_description),
trailing = {
Switch(isErrorReportingEnabled, { viewModel.onErrorReportingSelected(it) }, modifier = Modifier.height(32.dp.scaledHeight()).width(52.dp.scaledWidth()))
Switch(isErrorReportingEnabled, { showErrorReportingDialog = true }, modifier = Modifier.height(32.dp.scaledHeight()).width(52.dp.scaledWidth()))
})))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import okio.BufferedSource

object LicenseParser {
fun decode(source: BufferedSource): List<Artifact> {
return Json.decodeFromString(source.readString(Charsets.UTF_8))
return Json.decodeFromString<List<Artifact>>(source.readString(Charsets.UTF_8)).distinctBy { it.name }
}
}
Loading

0 comments on commit e5b2ebc

Please sign in to comment.