feat: add logs screen

This commit is contained in:
Zane Schepke 2024-03-13 02:52:57 -04:00
parent c0cff297b2
commit 4fc8ffbcbb
38 changed files with 786 additions and 249 deletions

View File

@ -132,6 +132,9 @@ android {
val generalImplementation by configurations val generalImplementation by configurations
dependencies { dependencies {
implementation(project(":logcatter"))
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
// optional - helpers for implementing LifecycleOwner in a Service // optional - helpers for implementing LifecycleOwner in a Service

View File

@ -5,6 +5,7 @@ import android.content.ComponentName
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber import timber.log.Timber
@ -13,7 +14,7 @@ class WireGuardAutoTunnel : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
} }
companion object { companion object {

View File

@ -56,7 +56,7 @@ open class ForegroundService : LifecycleService() {
stopForeground(STOP_FOREGROUND_REMOVE) stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf() stopSelf()
} catch (e: Exception) { } catch (e: Exception) {
Timber.d("Service stopped without being started: ${e.message}") Timber.e(e)
} }
isServiceStarted = false isServiceStarted = false
} }

View File

@ -154,10 +154,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
try { try {
if (isBatterySaverOn) { if (isBatterySaverOn) {
Timber.d("Initiating wakelock with timeout") Timber.i("Initiating wakelock with 10 min timeout")
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT) acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
} else { } else {
Timber.d("Initiating wakelock with zero timeout") Timber.i("Initiating wakelock with 30 min timeout")
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT) acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
} }
} finally { } finally {
@ -178,31 +178,31 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val setting = settingsRepository.getSettings() val setting = settingsRepository.getSettings()
launch { launch {
Timber.d("Starting wifi watcher") Timber.i("Starting wifi watcher")
watchForWifiConnectivityChanges() watchForWifiConnectivityChanges()
} }
if (setting.isTunnelOnMobileDataEnabled) { if (setting.isTunnelOnMobileDataEnabled) {
launch { launch {
Timber.d("Starting mobile data watcher") Timber.i("Starting mobile data watcher")
watchForMobileDataConnectivityChanges() watchForMobileDataConnectivityChanges()
} }
} }
if (setting.isTunnelOnEthernetEnabled) { if (setting.isTunnelOnEthernetEnabled) {
launch { launch {
Timber.d("Starting ethernet data watcher") Timber.i("Starting ethernet data watcher")
watchForEthernetConnectivityChanges() watchForEthernetConnectivityChanges()
} }
} }
launch { launch {
Timber.d("Starting vpn state watcher") Timber.i("Starting vpn state watcher")
watchForVpnConnectivityChanges() watchForVpnConnectivityChanges()
} }
launch { launch {
Timber.d("Starting settings watcher") Timber.i("Starting settings watcher")
watchForSettingsChanges() watchForSettingsChanges()
} }
launch { launch {
Timber.d("Starting management watcher") Timber.i("Starting management watcher")
manageVpn() manageVpn()
} }
} }
@ -212,7 +212,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
mobileDataService.networkStatus.collect { mobileDataService.networkStatus.collect {
when (it) { when (it) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.d("Gained Mobile data connection") Timber.i("Gained Mobile data connection")
networkEventsFlow.value = networkEventsFlow.value =
networkEventsFlow.value.copy( networkEventsFlow.value.copy(
isMobileDataConnected = true, isMobileDataConnected = true,
@ -223,14 +223,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
networkEventsFlow.value.copy( networkEventsFlow.value.copy(
isMobileDataConnected = true, isMobileDataConnected = true,
) )
Timber.d("Mobile data capabilities changed") Timber.i("Mobile data capabilities changed")
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.value = networkEventsFlow.value =
networkEventsFlow.value.copy( networkEventsFlow.value.copy(
isMobileDataConnected = false, isMobileDataConnected = false,
) )
Timber.d("Lost mobile data connection") Timber.i("Lost mobile data connection")
} }
} }
} }
@ -273,14 +273,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
ethernetService.networkStatus.collect { ethernetService.networkStatus.collect {
when (it) { when (it) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.d("Gained Ethernet connection") Timber.i("Gained Ethernet connection")
networkEventsFlow.value = networkEventsFlow.value =
networkEventsFlow.value.copy( networkEventsFlow.value.copy(
isEthernetConnected = true, isEthernetConnected = true,
) )
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Ethernet capabilities changed") Timber.i("Ethernet capabilities changed")
networkEventsFlow.value = networkEventsFlow.value =
networkEventsFlow.value.copy( networkEventsFlow.value.copy(
isEthernetConnected = true, isEthernetConnected = true,
@ -291,7 +291,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
networkEventsFlow.value.copy( networkEventsFlow.value.copy(
isEthernetConnected = false, isEthernetConnected = false,
) )
Timber.d("Lost Ethernet connection") Timber.i("Lost Ethernet connection")
} }
} }
} }
@ -301,20 +301,20 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
wifiService.networkStatus.collect { wifiService.networkStatus.collect {
when (it) { when (it) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.d("Gained Wi-Fi connection") Timber.i("Gained Wi-Fi connection")
networkEventsFlow.value = networkEventsFlow.value =
networkEventsFlow.value.copy( networkEventsFlow.value.copy(
isWifiConnected = true, isWifiConnected = true,
) )
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Wifi capabilities changed") Timber.i("Wifi capabilities changed")
networkEventsFlow.value = networkEventsFlow.value =
networkEventsFlow.value.copy( networkEventsFlow.value.copy(
isWifiConnected = true, isWifiConnected = true,
) )
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: "" val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
Timber.d("Detected SSID: $ssid") Timber.i("Detected SSID: $ssid")
networkEventsFlow.value = networkEventsFlow.value =
networkEventsFlow.value.copy( networkEventsFlow.value.copy(
currentNetworkSSID = ssid, currentNetworkSSID = ssid,
@ -325,7 +325,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
networkEventsFlow.value.copy( networkEventsFlow.value.copy(
isWifiConnected = false, isWifiConnected = false,
) )
Timber.d("Lost Wi-Fi connection") Timber.i("Lost Wi-Fi connection")
} }
} }
} }

View File

@ -68,7 +68,7 @@ class WireGuardTunnelService : ForegroundService() {
stopService(extras) stopService(extras)
} }
} else { } else {
Timber.d("Tunnel config null, starting default tunnel or first") Timber.i("Tunnel config null, starting default tunnel or first")
val settings = settingsRepository.getSettings() val settings = settingsRepository.getSettings()
val tunnels = tunnelConfigRepository.getAll() val tunnels = tunnelConfigRepository.getAll()
if (settings.isAlwaysOnVpnEnabled) { if (settings.isAlwaysOnVpnEnabled) {

View File

@ -1,12 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class ActivityViewModel
@Inject
constructor() : ViewModel() {
}

View File

@ -0,0 +1,70 @@
package com.zaneschepke.wireguardautotunnel.ui
import android.app.Application
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import androidx.lifecycle.ViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class AppViewModel
@Inject
constructor(
private val application: Application,
) : ViewModel() {
private val _snackbarState = MutableStateFlow(SnackBarState())
val snackBarState = _snackbarState.asStateFlow()
fun openWebPage(url: String) {
try {
val webpage: Uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
application.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Timber.e(e)
showSnackbarMessage(application.getString(R.string.no_browser_detected))
}
}
fun launchEmail() {
try {
val intent =
Intent(Intent.ACTION_SENDTO).apply {
type = Constants.EMAIL_MIME_TYPE
putExtra(Intent.EXTRA_EMAIL, arrayOf(application.getString(R.string.my_email)))
putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject))
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
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))
}
}
fun showSnackbarMessage(message : String) {
_snackbarState.value = _snackbarState.value.copy(
snackbarMessage = message,
snackbarMessageConsumed = false
)
}
fun snackbarMessageConsumed() {
_snackbarState.value = _snackbarState.value.copy(
snackbarMessage = "",
snackbarMessageConsumed = true
)
}
}

View File

@ -22,6 +22,7 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
@ -29,6 +30,8 @@ import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
@ -47,8 +50,10 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -64,7 +69,7 @@ class MainActivity : AppCompatActivity() {
@Inject lateinit var settingsRepository: SettingsRepository @Inject lateinit var settingsRepository: SettingsRepository
@OptIn( @OptIn(
ExperimentalPermissionsApi::class, ExperimentalPermissionsApi::class
) )
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -81,8 +86,8 @@ class MainActivity : AppCompatActivity() {
} }
} }
setContent { setContent {
//val activityViewModel = hiltViewModel<ActivityViewModel>() val appViewModel = hiltViewModel<AppViewModel>()
val snackBarState by appViewModel.snackBarState.collectAsStateWithLifecycle()
val navController = rememberNavController() val navController = rememberNavController()
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@ -104,12 +109,11 @@ class MainActivity : AppCompatActivity() {
requestNotificationPermission() requestNotificationPermission()
} }
fun showSnackBarMessage(message: String) { fun showSnackBarMessage(message: StringValue) {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
val result = val result =
snackbarHostState.showSnackbar( snackbarHostState.showSnackbar(
message = message, message = message.asString(this@MainActivity),
actionLabel = applicationContext.getString(R.string.okay),
duration = SnackbarDuration.Short, duration = SnackbarDuration.Short,
) )
when (result) { when (result) {
@ -121,6 +125,13 @@ class MainActivity : AppCompatActivity() {
} }
} }
LaunchedEffect(snackBarState.snackbarMessageConsumed) {
if(!snackBarState.snackbarMessageConsumed) {
showSnackBarMessage(StringValue.DynamicString(snackBarState.snackbarMessage))
appViewModel.snackbarMessageConsumed()
}
}
Scaffold( Scaffold(
snackbarHost = { snackbarHost = {
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData -> SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
@ -173,48 +184,48 @@ class MainActivity : AppCompatActivity() {
return@Scaffold return@Scaffold
} }
} }
NavHost(navController, startDestination = Screen.Main.route) { Column(modifier = Modifier.padding(padding)) {
composable( NavHost(navController, startDestination = Screen.Main.route) {
Screen.Main.route, composable(
) { Screen.Main.route,
MainScreen( ) {
focusRequester = focusRequester, MainScreen(
showSnackbarMessage = { message -> showSnackBarMessage(message) }, focusRequester = focusRequester,
navController = navController, appViewModel = appViewModel,
)
}
composable(
Screen.Settings.route,
) {
SettingsScreen(
padding = padding,
showSnackbarMessage = { message -> showSnackBarMessage(message) },
focusRequester = focusRequester,
)
//
}
composable(
Screen.Support.route,
) {
SupportScreen(
padding = padding,
focusRequester = focusRequester,
showSnackbarMessage = { message -> showSnackBarMessage(message) },
)
}
composable("${Screen.Config.route}/{id}") {
val id = it.arguments?.getString("id")
if (!id.isNullOrBlank()) {
ConfigScreen(
padding = padding,
navController = navController, navController = navController,
id = id, )
showSnackbarMessage = { message -> }
showSnackBarMessage(message) composable(
}, Screen.Settings.route,
) {
SettingsScreen(
appViewModel = appViewModel,
focusRequester = focusRequester, focusRequester = focusRequester,
) )
} }
composable(
Screen.Support.route,
) {
SupportScreen(
focusRequester = focusRequester,
appViewModel = appViewModel,
navController = navController
)
}
composable(Screen.Support.Logs.route,) {
LogsScreen()
}
composable("${Screen.Config.route}/{id}") {
val id = it.arguments?.getString("id")
if (!id.isNullOrBlank()) {
ConfigScreen(
navController = navController,
id = id,
appViewModel = appViewModel,
focusRequester = focusRequester,
)
}
}
} }
} }
} }

View File

@ -32,6 +32,7 @@ sealed class Screen(val route: String) {
route = route, route = route,
icon = Icons.Rounded.QuestionMark, icon = Icons.Rounded.QuestionMark,
) )
data object Logs : Screen("support/logs")
} }
data object Config : Screen("config") data object Config : Screen("config")

View File

@ -0,0 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui
data class SnackBarState(
val snackbarMessage: String = "",
val snackbarMessageConsumed: Boolean = true,
)

View File

@ -0,0 +1,20 @@
package com.zaneschepke.wireguardautotunnel.ui.common.text
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun LogTypeLabel(color : Color, content: @Composable () -> Unit,) {
Box(
modifier = Modifier.size(20.dp).clip(RoundedCornerShape(2.dp)).background(color), contentAlignment = Alignment.Center) {
content()
}
}

View File

@ -7,7 +7,6 @@ import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@ -73,6 +72,7 @@ import androidx.navigation.NavController
import com.google.accompanist.drawablepainter.DrawablePainter import com.google.accompanist.drawablepainter.DrawablePainter
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
@ -90,11 +90,10 @@ import kotlinx.coroutines.delay
) )
@Composable @Composable
fun ConfigScreen( fun ConfigScreen(
padding: PaddingValues,
viewModel: ConfigViewModel = hiltViewModel(), viewModel: ConfigViewModel = hiltViewModel(),
focusRequester: FocusRequester, focusRequester: FocusRequester,
navController: NavController, navController: NavController,
showSnackbarMessage: (String) -> Unit, appViewModel: AppViewModel,
id: String id: String
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -149,11 +148,11 @@ fun ConfigScreen(
}, },
onError = { onError = {
showAuthPrompt = false showAuthPrompt = false
showSnackbarMessage(Event.Error.AuthenticationFailed.message) appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
}, },
onFailure = { onFailure = {
showAuthPrompt = false showAuthPrompt = false
showSnackbarMessage(Event.Error.AuthorizationFailed.message) appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
}, },
) )
} }
@ -311,7 +310,7 @@ fun ConfigScreen(
var fobColor by remember { mutableStateOf(secondaryColor) } var fobColor by remember { mutableStateOf(secondaryColor) }
FloatingActionButton( FloatingActionButton(
modifier = modifier =
Modifier.padding(bottom = 90.dp).onFocusChanged { Modifier.onFocusChanged {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
fobColor = if (it.isFocused) hoverColor else secondaryColor fobColor = if (it.isFocused) hoverColor else secondaryColor
} }
@ -320,10 +319,10 @@ fun ConfigScreen(
viewModel.onSaveAllChanges().let { viewModel.onSaveAllChanges().let {
when (it) { when (it) {
is Result.Success -> { is Result.Success -> {
showSnackbarMessage(it.data.message) appViewModel.showSnackbarMessage(it.data.message)
navController.navigate(Screen.Main.route) navController.navigate(Screen.Main.route)
} }
is Result.Error -> showSnackbarMessage(it.error.message) is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
} }
} }
}, },
@ -346,8 +345,7 @@ fun ConfigScreen(
Modifier Modifier
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.weight(1f, true) .weight(1f, true)
.fillMaxSize() .fillMaxSize(),
.padding(padding),
) { ) {
Surface( Surface(
tonalElevation = 2.dp, tonalElevation = 2.dp,

View File

@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -216,6 +217,7 @@ constructor(
updateTunnelConfig(tunnelConfig) updateTunnelConfig(tunnelConfig)
Result.Success(Event.Message.ConfigSaved) Result.Success(Event.Message.ConfigSaved)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e)
Result.Error(Event.Error.Exception(e)) Result.Error(Event.Error.Exception(e))
} }
} }

View File

@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -20,11 +21,9 @@ import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@ -50,13 +49,11 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -76,7 +73,6 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -94,6 +90,7 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
import com.zaneschepke.wireguardautotunnel.ui.Screen import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
@ -109,12 +106,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
fun MainScreen( fun MainScreen(
viewModel: MainViewModel = hiltViewModel(), viewModel: MainViewModel = hiltViewModel(),
appViewModel: AppViewModel,
focusRequester: FocusRequester, focusRequester: FocusRequester,
showSnackbarMessage: (String) -> Unit,
navController: NavController navController: NavController
) { ) {
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
@ -182,7 +180,7 @@ fun MainScreen(
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB) name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
} }
) { ) {
showSnackbarMessage(Event.Error.FileExplorerRequired.message) appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message)
} }
return intent return intent
} }
@ -192,7 +190,7 @@ fun MainScreen(
scope.launch { scope.launch {
viewModel.onTunnelFileSelected(data).let { viewModel.onTunnelFileSelected(data).let {
when (it) { when (it) {
is Result.Error -> showSnackbarMessage(it.error.message) is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
is Result.Success -> {} is Result.Success -> {}
} }
} }
@ -207,7 +205,7 @@ fun MainScreen(
viewModel.onTunnelQrResult(it.contents).let { result -> viewModel.onTunnelQrResult(it.contents).let { result ->
when (result) { when (result) {
is Result.Success -> {} is Result.Success -> {}
is Result.Error -> showSnackbarMessage(result.error.message) is Result.Error -> appViewModel.showSnackbarMessage(result.error.message)
} }
} }
} }
@ -280,58 +278,6 @@ fun MainScreen(
) )
}, },
floatingActionButtonPosition = FabPosition.End, floatingActionButtonPosition = FabPosition.End,
topBar = {
if (uiState.settings.isAutoTunnelEnabled)
TopAppBar(
title = {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.requiredWidth(LocalConfiguration.current.screenWidthDp.dp)
.padding(end = 5.dp)
) {
Row {
Icon(
Icons.Rounded.Bolt,
stringResource(id = R.string.auto),
modifier = Modifier.size(25.dp),
tint =
if (uiState.settings.isAutoTunnelPaused) Color.Gray
else mint,
)
val autoTunnelingLabel = buildAnnotatedString {
append(stringResource(id = R.string.auto_tunneling))
append(": ")
if(uiState.settings.isAutoTunnelPaused) append(stringResource(id = R.string.paused)) else append(
stringResource(id = R.string.active),
)
}
Text(
autoTunnelingLabel.text,
style = typography.bodyLarge,
modifier = Modifier.padding(start = 10.dp),
)
}
if (uiState.settings.isAutoTunnelPaused)
TextButton(
onClick = { viewModel.resumeAutoTunneling() },
modifier = Modifier.padding(end = 10.dp),
) {
Text(stringResource(id = R.string.resume))
}
else
TextButton(
onClick = { viewModel.pauseAutoTunneling() },
modifier = Modifier.padding(end = 10.dp),
) {
Text(stringResource(id = R.string.pause))
}
}
},
)
},
floatingActionButton = { floatingActionButton = {
AnimatedVisibility( AnimatedVisibility(
visible = isVisible.value, visible = isVisible.value,
@ -349,7 +295,6 @@ fun MainScreen(
) )
Modifier.focusRequester(focusRequester) Modifier.focusRequester(focusRequester)
else Modifier) else Modifier)
.padding(bottom = 90.dp)
.onFocusChanged { .onFocusChanged {
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
fobColor = if (it.isFocused) hoverColor else secondaryColor fobColor = if (it.isFocused) hoverColor else secondaryColor
@ -367,14 +312,13 @@ fun MainScreen(
} }
} }
}, },
) { innerPadding -> ) {
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) { AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(innerPadding),
) { ) {
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic) Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
} }
@ -471,14 +415,57 @@ fun MainScreen(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight(.90f) .overscroll(ScrollableDefaults.overscrollEffect()),
.overscroll(ScrollableDefaults.overscrollEffect())
.padding(innerPadding),
state = rememberLazyListState(0, uiState.tunnels.count()), state = rememberLazyListState(0, uiState.tunnels.count()),
userScrollEnabled = true, userScrollEnabled = true,
reverseLayout = true, reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(), flingBehavior = ScrollableDefaults.flingBehavior(),
) { ) {
item {
if(uiState.settings.isAutoTunnelEnabled){
val autoTunnelingLabel = buildAnnotatedString {
append(stringResource(id = R.string.auto_tunneling))
append(": ")
if(uiState.settings.isAutoTunnelPaused) append(
stringResource(id = R.string.paused)
) else append(
stringResource(id = R.string.active),
)
}
RowListItem(
icon = { Icon(
Icons.Rounded.Bolt,
stringResource(id = R.string.auto),
modifier = Modifier
.padding(end = 10.dp)
.size(25.dp),
tint =
if (uiState.settings.isAutoTunnelPaused) Color.Gray
else mint,
) },
text = autoTunnelingLabel.text,
rowButton = {
if (uiState.settings.isAutoTunnelPaused) {
TextButton(
onClick = { viewModel.resumeAutoTunneling() },
) {
Text(stringResource(id = R.string.resume))
}
} else {
TextButton(
onClick = { viewModel.pauseAutoTunneling() },
) {
Text(stringResource(id = R.string.pause))
}
}
},
onClick = {},
onHold = {},
expanded = false,
statistics = null,
)
}
}
items( items(
uiState.tunnels, uiState.tunnels,
key = { tunnel -> tunnel.id }, key = { tunnel -> tunnel.id },
@ -534,7 +521,7 @@ fun MainScreen(
(uiState.vpnState.status == Tunnel.State.UP) && (uiState.vpnState.status == Tunnel.State.UP) &&
(tunnel.name == uiState.vpnState.name) (tunnel.name == uiState.vpnState.name)
) { ) {
showSnackbarMessage(Event.Message.TunnelOffAction.message) appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
return@RowListItem return@RowListItem
} }
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
@ -568,7 +555,7 @@ fun MainScreen(
uiState.settings.isAutoTunnelEnabled && uiState.settings.isAutoTunnelEnabled &&
!uiState.settings.isAutoTunnelPaused !uiState.settings.isAutoTunnelPaused
) { ) {
showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message, Event.Message.AutoTunnelOffAction.message,
) )
} else { } else {
@ -591,7 +578,7 @@ fun MainScreen(
) && ) &&
!uiState.settings.isAutoTunnelPaused !uiState.settings.isAutoTunnelPaused
) { ) {
showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message, Event.Message.AutoTunnelOffAction.message,
) )
} else } else
@ -634,7 +621,7 @@ fun MainScreen(
IconButton( IconButton(
onClick = { onClick = {
if (uiState.settings.isAutoTunnelEnabled) { if (uiState.settings.isAutoTunnelEnabled) {
showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.AutoTunnelOffAction.message, Event.Message.AutoTunnelOffAction.message,
) )
} else { } else {
@ -658,7 +645,7 @@ fun MainScreen(
) { ) {
expanded.value = !expanded.value expanded.value = !expanded.value
} else { } else {
showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.TunnelOnAction.message Event.Message.TunnelOnAction.message
) )
} }
@ -672,7 +659,7 @@ fun MainScreen(
uiState.vpnState.status == Tunnel.State.UP && uiState.vpnState.status == Tunnel.State.UP &&
tunnel.name == uiState.vpnState.name tunnel.name == uiState.vpnState.name
) { ) {
showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.TunnelOffAction.message Event.Message.TunnelOffAction.message
) )
} else { } else {
@ -690,7 +677,7 @@ fun MainScreen(
uiState.vpnState.status == Tunnel.State.UP && uiState.vpnState.status == Tunnel.State.UP &&
tunnel.name == uiState.vpnState.name tunnel.name == uiState.vpnState.name
) { ) {
showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Message.TunnelOffAction.message Event.Message.TunnelOffAction.message
) )
} else { } else {

View File

@ -122,6 +122,7 @@ constructor(
addTunnel(tunnelConfig) addTunnel(tunnelConfig)
Result.Success(Unit) Result.Success(Unit)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e)
Result.Error(Event.Error.InvalidQrCode) Result.Error(Event.Error.InvalidQrCode)
} }
} }
@ -158,6 +159,7 @@ constructor(
return Result.Error(Event.Error.InvalidFileExtension) return Result.Error(Event.Error.InvalidFileExtension)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e)
return Result.Error(Event.Error.FileReadFailed) return Result.Error(Event.Error.FileReadFailed)
} }
} }
@ -249,6 +251,7 @@ constructor(
return try { return try {
fileName.substring(fileName.lastIndexOf('.')) fileName.substring(fileName.lastIndexOf('.'))
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e)
"" ""
} }
} }

View File

@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -76,6 +75,7 @@ import com.wireguard.android.backend.Tunnel
import com.wireguard.android.backend.WgQuickBackend import com.wireguard.android.backend.WgQuickBackend
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
@ -86,6 +86,7 @@ import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.Result import com.zaneschepke.wireguardautotunnel.util.Result
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File import java.io.File
@OptIn( @OptIn(
@ -94,9 +95,8 @@ import java.io.File
) )
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
padding: PaddingValues,
viewModel: SettingsViewModel = hiltViewModel(), viewModel: SettingsViewModel = hiltViewModel(),
showSnackbarMessage: (String) -> Unit, appViewModel: AppViewModel,
focusRequester: FocusRequester focusRequester: FocusRequester
) { ) {
val scope = rememberCoroutineScope { Dispatchers.IO } val scope = rememberCoroutineScope { Dispatchers.IO }
@ -141,9 +141,10 @@ fun SettingsScreen(
} }
FileUtils.saveFilesToZip(context, files) FileUtils.saveFilesToZip(context, files)
didExportFiles = true didExportFiles = true
showSnackbarMessage(Event.Message.ConfigsExported.message) appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message)
} catch (e: Exception) { } catch (e: Exception) {
showSnackbarMessage(Event.Error.Exception(e).message) Timber.e(e)
appViewModel.showSnackbarMessage(Event.Error.Exception(e).message)
} }
} }
@ -174,7 +175,7 @@ fun SettingsScreen(
viewModel.onSaveTrustedSSID(currentText).let { viewModel.onSaveTrustedSSID(currentText).let {
when (it) { when (it) {
is Result.Success -> currentText = "" is Result.Success -> currentText = ""
is Result.Error -> showSnackbarMessage(it.error.message) is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
} }
} }
} }
@ -248,7 +249,7 @@ fun SettingsScreen(
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding), modifier = Modifier.fillMaxSize().verticalScroll(scrollState),
) { ) {
Icon( Icon(
Icons.Rounded.LocationOff, Icons.Rounded.LocationOff,
@ -301,11 +302,11 @@ fun SettingsScreen(
}, },
onError = { _ -> onError = { _ ->
showAuthPrompt = false showAuthPrompt = false
showSnackbarMessage(Event.Error.AuthenticationFailed.message) appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
}, },
onFailure = { onFailure = {
showAuthPrompt = false showAuthPrompt = false
showSnackbarMessage(Event.Error.AuthorizationFailed.message) appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
}, },
) )
} }
@ -314,7 +315,7 @@ fun SettingsScreen(
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().padding(padding), modifier = Modifier.fillMaxSize(),
) { ) {
Text( Text(
stringResource(R.string.one_tunnel_required), stringResource(R.string.one_tunnel_required),
@ -329,7 +330,7 @@ fun SettingsScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier.fillMaxSize().padding(padding).verticalScroll(scrollState).clickable( Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
indication = null, indication = null,
interactionSource = interactionSource, interactionSource = interactionSource,
) { ) {
@ -501,11 +502,11 @@ fun SettingsScreen(
) { ) {
when (false) { when (false) {
isBackgroundLocationGranted -> isBackgroundLocationGranted ->
showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Error.BackgroundLocationRequired.message Event.Error.BackgroundLocationRequired.message
) )
fineLocationState.status.isGranted -> fineLocationState.status.isGranted ->
showSnackbarMessage( appViewModel.showSnackbarMessage(
Event.Error.PreciseLocationRequired.message Event.Error.PreciseLocationRequired.message
) )
viewModel.isLocationEnabled(context) -> viewModel.isLocationEnabled(context) ->
@ -558,7 +559,7 @@ fun SettingsScreen(
onCheckChanged = { onCheckChanged = {
viewModel.onToggleKernelMode().let { viewModel.onToggleKernelMode().let {
when (it) { when (it) {
is Result.Error -> showSnackbarMessage(it.error.message) is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
is Result.Success -> {} is Result.Success -> {}
} }
} }

View File

@ -183,9 +183,10 @@ constructor(
if (!uiState.value.settings.isKernelEnabled) { if (!uiState.value.settings.isKernelEnabled) {
try { try {
rootShell.start() rootShell.start()
Timber.d("Root shell accepted!") Timber.i("Root shell accepted!")
saveKernelMode(on = true) saveKernelMode(on = true)
} catch (e: RootShell.RootShellException) { } catch (e: RootShell.RootShellException) {
Timber.e(e)
saveKernelMode(on = false) saveKernelMode(on = false)
return Result.Error(Event.Error.RootDenied) return Result.Error(Event.Error.RootDenied)
} }

View File

@ -1,14 +1,10 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support package com.zaneschepke.wireguardautotunnel.ui.screens.support
import android.content.Intent
import android.content.Intent.createChooser
import android.net.Uri
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -22,6 +18,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowForward import androidx.compose.material.icons.automirrored.rounded.ArrowForward
import androidx.compose.material.icons.rounded.Book import androidx.compose.material.icons.rounded.Book
import androidx.compose.material.icons.rounded.FormatListNumbered
import androidx.compose.material.icons.rounded.Mail import androidx.compose.material.icons.rounded.Mail
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -45,21 +42,21 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.zaneschepke.wireguardautotunnel.BuildConfig import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.Event
@Composable @Composable
fun SupportScreen( fun SupportScreen(
padding: PaddingValues,
viewModel: SupportViewModel = hiltViewModel(), viewModel: SupportViewModel = hiltViewModel(),
showSnackbarMessage: (String) -> Unit, appViewModel: AppViewModel,
navController: NavController,
focusRequester: FocusRequester focusRequester: FocusRequester
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -67,34 +64,6 @@ fun SupportScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
fun openWebPage(url: String) {
try {
val webpage: Uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, webpage)
context.startActivity(intent)
} catch (e: Exception) {
showSnackbarMessage(Event.Error.Exception(e).message)
}
}
fun launchEmail() {
try {
val intent =
Intent(Intent.ACTION_SENDTO).apply {
type = Constants.EMAIL_MIME_TYPE
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
}
startActivity(
context,
createChooser(intent, context.getString(R.string.email_chooser)),
null,
)
} catch (e: Exception) {
showSnackbarMessage(Event.Error.Exception(e).message)
}
}
if (uiState.loading) { if (uiState.loading) {
LoadingScreen() LoadingScreen()
return return
@ -104,9 +73,10 @@ fun SupportScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier.fillMaxSize().padding(padding) Modifier
.verticalScroll(rememberScrollState()) .fillMaxSize()
.focusable() .verticalScroll(rememberScrollState())
.focusable()
) { ) {
Surface( Surface(
tonalElevation = 2.dp, tonalElevation = 2.dp,
@ -115,15 +85,19 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
modifier = modifier =
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) { (if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
Modifier.height(IntrinsicSize.Min) Modifier
.fillMaxWidth(fillMaxWidth) .height(IntrinsicSize.Min)
.padding(top = 10.dp) .fillMaxWidth(fillMaxWidth)
.padding(top = 10.dp)
} else { } else {
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp) Modifier
.fillMaxWidth(fillMaxWidth)
.padding(top = 20.dp)
}) })
.padding(bottom = 25.dp), .padding(bottom = 25.dp),
) { ) {
Column(modifier = Modifier.padding(20.dp)) { Column(modifier = Modifier.padding(20.dp)) {
val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward
Text( Text(
stringResource(R.string.thank_you), stringResource(R.string.thank_you),
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
@ -138,8 +112,10 @@ fun SupportScreen(
modifier = Modifier.padding(bottom = 20.dp), modifier = Modifier.padding(bottom = 20.dp),
) )
TextButton( TextButton(
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) }, onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) },
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester), modifier = Modifier
.padding(vertical = 5.dp)
.focusRequester(focusRequester),
) { ) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@ -147,7 +123,8 @@ fun SupportScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Row { Row {
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs)) val icon = Icons.Rounded.Book
Icon(icon, icon.name)
Text( Text(
stringResource(id = R.string.docs_description), stringResource(id = R.string.docs_description),
textAlign = TextAlign.Justify, textAlign = TextAlign.Justify,
@ -155,8 +132,8 @@ fun SupportScreen(
) )
} }
Icon( Icon(
Icons.AutoMirrored.Rounded.ArrowForward, forwardIcon,
stringResource(id = R.string.go) forwardIcon.name
) )
} }
} }
@ -165,7 +142,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground
) )
TextButton( TextButton(
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) }, onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) },
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
) { ) {
Row( Row(
@ -174,9 +151,10 @@ fun SupportScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Row { Row {
val icon = ImageVector.vectorResource(R.drawable.discord)
Icon( Icon(
imageVector = ImageVector.vectorResource(R.drawable.discord), icon,
stringResource(id = R.string.discord), icon.name,
Modifier.size(25.dp), Modifier.size(25.dp),
) )
Text( Text(
@ -186,8 +164,8 @@ fun SupportScreen(
) )
} }
Icon( Icon(
Icons.AutoMirrored.Rounded.ArrowForward, forwardIcon,
stringResource(id = R.string.go) forwardIcon.name
) )
} }
} }
@ -196,7 +174,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground
) )
TextButton( TextButton(
onClick = { openWebPage(context.resources.getString(R.string.github_url)) }, onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) },
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
) { ) {
Row( Row(
@ -205,20 +183,21 @@ fun SupportScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Row { Row {
val icon = ImageVector.vectorResource(R.drawable.github)
Icon( Icon(
imageVector = ImageVector.vectorResource(R.drawable.github), imageVector = icon,
stringResource(id = R.string.github), icon.name,
Modifier.size(25.dp), Modifier.size(25.dp),
) )
Text( Text(
"Open an issue", stringResource(id = R.string.open_issue),
textAlign = TextAlign.Justify, textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp), modifier = Modifier.padding(start = 10.dp),
) )
} }
Icon( Icon(
Icons.AutoMirrored.Rounded.ArrowForward, forwardIcon,
stringResource(id = R.string.go) forwardIcon.name
) )
} }
} }
@ -227,7 +206,7 @@ fun SupportScreen(
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground
) )
TextButton( TextButton(
onClick = { launchEmail() }, onClick = { appViewModel.launchEmail() },
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.padding(vertical = 5.dp),
) { ) {
Row( Row(
@ -236,13 +215,42 @@ fun SupportScreen(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Row { Row {
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email)) val icon = Icons.Rounded.Mail
Icon(icon, icon.name)
Text( Text(
stringResource(id = R.string.email_description), stringResource(id = R.string.email_description),
textAlign = TextAlign.Justify, textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp), modifier = Modifier.padding(start = 10.dp),
) )
} }
Icon(
forwardIcon,
forwardIcon.name
)
}
}
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.onBackground
)
TextButton(
onClick = { navController.navigate(Screen.Support.Logs.route) },
modifier = Modifier.padding(vertical = 5.dp),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Row {
val icon = Icons.Rounded.FormatListNumbered
Icon(icon, icon.name)
Text(
stringResource(id = R.string.read_logs),
textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 10.dp),
)
}
Icon( Icon(
Icons.AutoMirrored.Rounded.ArrowForward, Icons.AutoMirrored.Rounded.ArrowForward,
stringResource(id = R.string.go) stringResource(id = R.string.go)
@ -258,7 +266,7 @@ fun SupportScreen(
fontSize = 16.sp, fontSize = 16.sp,
modifier = modifier =
Modifier.clickable { Modifier.clickable {
openWebPage(context.resources.getString(R.string.privacy_policy_url)) appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url))
}, },
) )
Row( Row(

View File

@ -0,0 +1,108 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun LogsScreen(logsViewModel: LogsViewModel = hiltViewModel()) {
val logs = remember {
logsViewModel.logs
}
val lazyColumnListState = rememberLazyListState()
val clipboardManager: ClipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
logsViewModel.readLogCatOutput()
}
LaunchedEffect(logs.size){
scope.launch {
lazyColumnListState.animateScrollToItem(logs.size)
}
}
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = {
logsViewModel.saveLogsToFile()
},
shape = RoundedCornerShape(16.dp),
containerColor = MaterialTheme.colorScheme.primary
) {
val icon = Icons.Filled.Save
Icon(
imageVector = icon,
contentDescription = icon.name,
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
state = lazyColumnListState,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp)) {
items(logs) {
Row(horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), verticalAlignment = Alignment.Top,
modifier = Modifier
.fillMaxSize()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {
clipboardManager.setText(annotatedString = AnnotatedString(it.toString()))
}
)
) {
val fontSize = 10.sp
Text(text = it.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize)
LogTypeLabel(color = Color(it.level.color())) {
Text(text = it.level.signifier, textAlign = TextAlign.Center, fontSize = fontSize)
}
Text("${it.message} - ${it.time}", fontSize = fontSize)
}
}
}
}
}

View File

@ -0,0 +1,53 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
import android.app.Application
import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.logcatter.Logcatter
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.Instant
import javax.inject.Inject
@HiltViewModel
class LogsViewModel
@Inject
constructor(
private val application: Application
) : ViewModel() {
val logs = mutableStateListOf<LogMessage>()
fun readLogCatOutput() = viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
launch {
Logcatter.logs {
logs.add(it)
if (logs.size > Constants.LOG_BUFFER_SIZE) {
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
}
}
}
}
fun clearLogs() {
logs.clear()
Logcatter.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)
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT).show()
}
}

View File

@ -1,17 +1,22 @@
package com.zaneschepke.wireguardautotunnel.util package com.zaneschepke.wireguardautotunnel.util
object Constants { object Constants {
const val BASE_LOG_FILE_NAME = "wgtunnel-logs"
const val LOG_BUFFER_SIZE = 3_000L
const val MANUAL_TUNNEL_CONFIG_ID = "0" const val MANUAL_TUNNEL_CONFIG_ID = "0"
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1000L // 30 minutes const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1_000L // 30 minutes
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3000L const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
const val TOGGLE_TUNNEL_DELAY = 300L const val TOGGLE_TUNNEL_DELAY = 300L
const val CONF_FILE_EXTENSION = ".conf" const val CONF_FILE_EXTENSION = ".conf"
const val ZIP_FILE_EXTENSION = ".zip" const val ZIP_FILE_EXTENSION = ".zip"
const val URI_CONTENT_SCHEME = "content" const val URI_CONTENT_SCHEME = "content"
const val URI_PACKAGE_SCHEME = "package" const val URI_PACKAGE_SCHEME = "package"
const val ALLOWED_FILE_TYPES = "*/*" const val ALLOWED_FILE_TYPES = "*/*"
const val TEXT_MIME_TYPE = "text/plain"
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs" const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs" const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
const val EMAIL_MIME_TYPE = "message/rfc822" const val EMAIL_MIME_TYPE = "message/rfc822"

View File

@ -7,6 +7,7 @@ import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.MediaStore.MediaColumns import android.provider.MediaStore.MediaColumns
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream import java.io.OutputStream
import java.time.Instant import java.time.Instant
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
@ -43,6 +44,31 @@ object FileUtils {
return null return null
} }
fun saveFileToDownloads(context: Context, content: String, fileName: String) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val resolver = context.contentResolver
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
resolver.openOutputStream(uri).use { output ->
output?.write(content.toByteArray())
}
}
} else {
val target = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
fileName
)
FileOutputStream(target).use { output ->
output.write(content.toByteArray())
}
}
}
fun saveFilesToZip(context: Context, files: List<File>) { fun saveFilesToZip(context: Context, files: List<File>) {
val zipOutputStream = val zipOutputStream =
createDownloadsFileOutputStream( createDownloadsFileOutputStream(

View File

@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.util
import android.util.Log
import timber.log.Timber
class ReleaseTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
when(priority) {
Log.DEBUG -> return
}
super.log(priority,tag,message,t)
}
}

View File

@ -0,0 +1,24 @@
package com.zaneschepke.wireguardautotunnel.util
import android.content.Context
import androidx.annotation.StringRes
sealed class StringValue {
data class DynamicString(val value: String) : StringValue()
data object Empty : StringValue()
class StringResource(
@StringRes val resId: Int,
vararg val args: Any
) : StringValue()
fun asString(context: Context?): String {
return when (this) {
is Empty -> ""
is DynamicString -> value
is StringResource -> context?.getString(resId, *args).orEmpty()
}
}
}

View File

@ -146,10 +146,6 @@
<string name="go">go</string> <string name="go">go</string>
<string name="docs_description">Read the docs (WIP)</string> <string name="docs_description">Read the docs (WIP)</string>
<string name="discord_description">Join the community</string> <string name="discord_description">Join the community</string>
<string name="discord" translatable="false">Discord</string>
<string name="docs">Docs</string>
<string name="github" translatable="false">GitHub</string>
<string name="email">Email</string>
<string name="email_description">Send me an email</string> <string name="email_description">Send me an email</string>
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string> <string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
<string name="kernel">Kernel</string> <string name="kernel">Kernel</string>
@ -176,4 +172,9 @@
<string name="excluded">excluded</string> <string name="excluded">excluded</string>
<string name="all">all</string> <string name="all">all</string>
<string name="always_on_disabled">Always-on VPN attempted to start a tunnel, but this feature is disabled in settings.</string> <string name="always_on_disabled">Always-on VPN attempted to start a tunnel, but this feature is disabled in settings.</string>
<string name="no_email_detected">No email app detected</string>
<string name="no_browser_detected">No browser detected</string>
<string name="logs_saved">Logs saved to downloads</string>
<string name="open_issue">Open an issue</string>
<string name="read_logs">Read the logs</string>
</resources> </resources>

View File

@ -4,4 +4,5 @@ plugins {
alias(libs.plugins.hilt.android) apply false alias(libs.plugins.hilt.android) apply false
alias(libs.plugins.kotlinxSerialization) apply false alias(libs.plugins.kotlinxSerialization) apply false
alias(libs.plugins.ksp) apply false alias(libs.plugins.ksp) apply false
alias(libs.plugins.androidLibrary) apply false
} }

View File

@ -29,6 +29,7 @@ zxingCore = "3.5.3"
#plugins #plugins
gradlePlugins-kotlinxSerialization = "1.8.21" gradlePlugins-kotlinxSerialization = "1.8.21"
material = "1.10.0"
[libraries] [libraries]
@ -84,10 +85,12 @@ tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel"
zxing-core = { module = "com.google.zxing:core", version.ref = "zxingCore" } zxing-core = { module = "com.google.zxing:core", version.ref = "zxingCore" }
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" } zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" } hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "gradlePlugins-kotlinxSerialization" } kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "gradlePlugins-kotlinxSerialization" }
androidLibrary = { id = "com.android.library", version.ref = "androidGradlePlugin" }

1
logcatter/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,43 @@
plugins {
alias(libs.plugins.androidLibrary)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.zaneschepke.logcatter"
compileSdk = 34
defaultConfig {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}

View File

21
logcatter/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,22 @@
package com.zaneschepke
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.zaneschepke.test", appContext.packageName)
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>

View File

@ -0,0 +1,19 @@
package com.zaneschepke.logcatter
import com.zaneschepke.logcatter.model.LogMessage
object Logcatter {
fun logs(callback: (input: LogMessage) -> Unit) {
clear()
Runtime.getRuntime().exec("logcat -v epoch")
.inputStream
.bufferedReader()
.useLines { lines ->
lines.forEach { callback(LogMessage.from(it)) }
}
}
fun clear() {
Runtime.getRuntime().exec("logcat -c")
}
}

View File

@ -0,0 +1,48 @@
package com.zaneschepke.logcatter.model
enum class LogLevel(val signifier: String) {
DEBUG("D") {
override fun color(): Long {
return 0xFF2196F3
}
},
INFO("I"){
override fun color(): Long {
return 0xFF4CAF50
}
},
ASSERT("A"){
override fun color(): Long {
return 0xFF9C27B0
}
},
WARNING("W"){
override fun color(): Long {
return 0xFFFFC107
}
},
ERROR("E"){
override fun color(): Long {
return 0xFFF44336
}
},
VERBOSE("V"){
override fun color(): Long {
return 0xFF000000
}
};
abstract fun color() : Long
companion object {
fun fromSignifier(signifier: String) : LogLevel {
return when(signifier) {
DEBUG.signifier -> DEBUG
INFO.signifier -> INFO
WARNING.signifier -> WARNING
VERBOSE.signifier -> VERBOSE
ASSERT.signifier -> ASSERT
ERROR.signifier -> ERROR
else -> VERBOSE
}
}
}
}

View File

@ -0,0 +1,28 @@
package com.zaneschepke.logcatter.model
import java.time.Instant
data class LogMessage(
val time: Instant,
val pid: String,
val tid: String,
val level : LogLevel,
val tag: String,
val message: String
) {
override fun toString(): String {
return "$time $pid $tid $level $tag message= $message"
}
companion object {
fun from(logcatLine : String) : LogMessage {
return if(logcatLine.contains("---------")) LogMessage(Instant.now(), "0","0",LogLevel.VERBOSE,"System", logcatLine)
else {
//TODO improve this
val parts = logcatLine.trim().split(" ").filter { it.isNotEmpty() }
val epochParts = parts[0].split(".").map { it.toLong() }
val message = parts.subList(5, parts.size).joinToString(" ")
LogMessage(Instant.ofEpochSecond(epochParts[0], epochParts[1]), parts[1], parts[2], LogLevel.fromSignifier(parts[3]), parts[4], message)
}
}
}
}

View File

@ -0,0 +1,16 @@
package com.zaneschepke
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -41,3 +41,4 @@ fun getLocalProperty(key: String, file: String = "local.properties"): String? {
rootProject.name = "WG Tunnel" rootProject.name = "WG Tunnel"
include(":app") include(":app")
include(":logcatter")