feat: add logs screen
This commit is contained in:
parent
c0cff297b2
commit
4fc8ffbcbb
|
@ -132,6 +132,9 @@ android {
|
|||
val generalImplementation by configurations
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(project(":logcatter"))
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
// optional - helpers for implementing LifecycleOwner in a Service
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.ComponentName
|
|||
import android.content.pm.PackageManager
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -13,7 +14,7 @@ class WireGuardAutoTunnel : Application() {
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -56,7 +56,7 @@ open class ForegroundService : LifecycleService() {
|
|||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
} catch (e: Exception) {
|
||||
Timber.d("Service stopped without being started: ${e.message}")
|
||||
Timber.e(e)
|
||||
}
|
||||
isServiceStarted = false
|
||||
}
|
||||
|
|
|
@ -154,10 +154,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||
try {
|
||||
if (isBatterySaverOn) {
|
||||
Timber.d("Initiating wakelock with timeout")
|
||||
Timber.i("Initiating wakelock with 10 min timeout")
|
||||
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||
} else {
|
||||
Timber.d("Initiating wakelock with zero timeout")
|
||||
Timber.i("Initiating wakelock with 30 min timeout")
|
||||
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||
}
|
||||
} finally {
|
||||
|
@ -178,31 +178,31 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val setting = settingsRepository.getSettings()
|
||||
launch {
|
||||
Timber.d("Starting wifi watcher")
|
||||
Timber.i("Starting wifi watcher")
|
||||
watchForWifiConnectivityChanges()
|
||||
}
|
||||
if (setting.isTunnelOnMobileDataEnabled) {
|
||||
launch {
|
||||
Timber.d("Starting mobile data watcher")
|
||||
Timber.i("Starting mobile data watcher")
|
||||
watchForMobileDataConnectivityChanges()
|
||||
}
|
||||
}
|
||||
if (setting.isTunnelOnEthernetEnabled) {
|
||||
launch {
|
||||
Timber.d("Starting ethernet data watcher")
|
||||
Timber.i("Starting ethernet data watcher")
|
||||
watchForEthernetConnectivityChanges()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.d("Starting vpn state watcher")
|
||||
Timber.i("Starting vpn state watcher")
|
||||
watchForVpnConnectivityChanges()
|
||||
}
|
||||
launch {
|
||||
Timber.d("Starting settings watcher")
|
||||
Timber.i("Starting settings watcher")
|
||||
watchForSettingsChanges()
|
||||
}
|
||||
launch {
|
||||
Timber.d("Starting management watcher")
|
||||
Timber.i("Starting management watcher")
|
||||
manageVpn()
|
||||
}
|
||||
}
|
||||
|
@ -212,7 +212,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
mobileDataService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Mobile data connection")
|
||||
Timber.i("Gained Mobile data connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
|
@ -223,14 +223,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
Timber.d("Mobile data capabilities changed")
|
||||
Timber.i("Mobile data capabilities changed")
|
||||
}
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = false,
|
||||
)
|
||||
Timber.d("Lost mobile data connection")
|
||||
Timber.i("Lost mobile data connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -273,14 +273,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
ethernetService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Ethernet connection")
|
||||
Timber.i("Gained Ethernet connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.d("Ethernet capabilities changed")
|
||||
Timber.i("Ethernet capabilities changed")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
|
@ -291,7 +291,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = false,
|
||||
)
|
||||
Timber.d("Lost Ethernet connection")
|
||||
Timber.i("Lost Ethernet connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -301,20 +301,20 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
wifiService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Wi-Fi connection")
|
||||
Timber.i("Gained Wi-Fi connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.d("Wifi capabilities changed")
|
||||
Timber.i("Wifi capabilities changed")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||
Timber.d("Detected SSID: $ssid")
|
||||
Timber.i("Detected SSID: $ssid")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
currentNetworkSSID = ssid,
|
||||
|
@ -325,7 +325,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = false,
|
||||
)
|
||||
Timber.d("Lost Wi-Fi connection")
|
||||
Timber.i("Lost Wi-Fi connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ class WireGuardTunnelService : ForegroundService() {
|
|||
stopService(extras)
|
||||
}
|
||||
} 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 tunnels = tunnelConfigRepository.getAll()
|
||||
if (settings.isAlwaysOnVpnEnabled) {
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ import androidx.compose.material3.SnackbarHostState
|
|||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
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.toArgb
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.NavHost
|
||||
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.settings.SettingsScreen
|
||||
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.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -64,7 +69,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
@OptIn(
|
||||
ExperimentalPermissionsApi::class,
|
||||
ExperimentalPermissionsApi::class
|
||||
)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -81,8 +86,8 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
setContent {
|
||||
//val activityViewModel = hiltViewModel<ActivityViewModel>()
|
||||
|
||||
val appViewModel = hiltViewModel<AppViewModel>()
|
||||
val snackBarState by appViewModel.snackBarState.collectAsStateWithLifecycle()
|
||||
val navController = rememberNavController()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
|
@ -104,12 +109,11 @@ class MainActivity : AppCompatActivity() {
|
|||
requestNotificationPermission()
|
||||
}
|
||||
|
||||
fun showSnackBarMessage(message: String) {
|
||||
fun showSnackBarMessage(message: StringValue) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val result =
|
||||
snackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
actionLabel = applicationContext.getString(R.string.okay),
|
||||
message = message.asString(this@MainActivity),
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
when (result) {
|
||||
|
@ -121,6 +125,13 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(snackBarState.snackbarMessageConsumed) {
|
||||
if(!snackBarState.snackbarMessageConsumed) {
|
||||
showSnackBarMessage(StringValue.DynamicString(snackBarState.snackbarMessage))
|
||||
appViewModel.snackbarMessageConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = {
|
||||
SnackbarHost(snackbarHostState) { snackbarData: SnackbarData ->
|
||||
|
@ -173,48 +184,48 @@ class MainActivity : AppCompatActivity() {
|
|||
return@Scaffold
|
||||
}
|
||||
}
|
||||
NavHost(navController, startDestination = Screen.Main.route) {
|
||||
composable(
|
||||
Screen.Main.route,
|
||||
) {
|
||||
MainScreen(
|
||||
focusRequester = focusRequester,
|
||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
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,
|
||||
Column(modifier = Modifier.padding(padding)) {
|
||||
NavHost(navController, startDestination = Screen.Main.route) {
|
||||
composable(
|
||||
Screen.Main.route,
|
||||
) {
|
||||
MainScreen(
|
||||
focusRequester = focusRequester,
|
||||
appViewModel = appViewModel,
|
||||
navController = navController,
|
||||
id = id,
|
||||
showSnackbarMessage = { message ->
|
||||
showSnackBarMessage(message)
|
||||
},
|
||||
)
|
||||
}
|
||||
composable(
|
||||
Screen.Settings.route,
|
||||
) {
|
||||
SettingsScreen(
|
||||
appViewModel = appViewModel,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ sealed class Screen(val route: String) {
|
|||
route = route,
|
||||
icon = Icons.Rounded.QuestionMark,
|
||||
)
|
||||
data object Logs : Screen("support/logs")
|
||||
}
|
||||
|
||||
data object Config : Screen("config")
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui
|
||||
|
||||
data class SnackBarState(
|
||||
val snackbarMessage: String = "",
|
||||
val snackbarMessageConsumed: Boolean = true,
|
||||
)
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@ import androidx.compose.foundation.focusGroup
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
|
@ -73,6 +72,7 @@ import androidx.navigation.NavController
|
|||
import com.google.accompanist.drawablepainter.DrawablePainter
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||
|
@ -90,11 +90,10 @@ import kotlinx.coroutines.delay
|
|||
)
|
||||
@Composable
|
||||
fun ConfigScreen(
|
||||
padding: PaddingValues,
|
||||
viewModel: ConfigViewModel = hiltViewModel(),
|
||||
focusRequester: FocusRequester,
|
||||
navController: NavController,
|
||||
showSnackbarMessage: (String) -> Unit,
|
||||
appViewModel: AppViewModel,
|
||||
id: String
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
@ -149,11 +148,11 @@ fun ConfigScreen(
|
|||
},
|
||||
onError = {
|
||||
showAuthPrompt = false
|
||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
},
|
||||
onFailure = {
|
||||
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) }
|
||||
FloatingActionButton(
|
||||
modifier =
|
||||
Modifier.padding(bottom = 90.dp).onFocusChanged {
|
||||
Modifier.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
|
@ -320,10 +319,10 @@ fun ConfigScreen(
|
|||
viewModel.onSaveAllChanges().let {
|
||||
when (it) {
|
||||
is Result.Success -> {
|
||||
showSnackbarMessage(it.data.message)
|
||||
appViewModel.showSnackbarMessage(it.data.message)
|
||||
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
|
||||
.verticalScroll(rememberScrollState())
|
||||
.weight(1f, true)
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
|
|
|
@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
|
@ -216,6 +217,7 @@ constructor(
|
|||
updateTunnelConfig(tunnelConfig)
|
||||
Result.Success(Event.Message.ConfigSaved)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Result.Error(Event.Error.Exception(e))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
|
@ -50,13 +49,11 @@ import androidx.compose.material3.HorizontalDivider
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.typography
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
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.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
@ -94,6 +90,7 @@ import com.zaneschepke.wireguardautotunnel.R
|
|||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||
|
@ -109,12 +106,13 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
viewModel: MainViewModel = hiltViewModel(),
|
||||
appViewModel: AppViewModel,
|
||||
focusRequester: FocusRequester,
|
||||
showSnackbarMessage: (String) -> Unit,
|
||||
navController: NavController
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
@ -182,7 +180,7 @@ fun MainScreen(
|
|||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||
}
|
||||
) {
|
||||
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||
appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
|
@ -192,7 +190,7 @@ fun MainScreen(
|
|||
scope.launch {
|
||||
viewModel.onTunnelFileSelected(data).let {
|
||||
when (it) {
|
||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
is Result.Success -> {}
|
||||
}
|
||||
}
|
||||
|
@ -207,7 +205,7 @@ fun MainScreen(
|
|||
viewModel.onTunnelQrResult(it.contents).let { result ->
|
||||
when (result) {
|
||||
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,
|
||||
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 = {
|
||||
AnimatedVisibility(
|
||||
visible = isVisible.value,
|
||||
|
@ -349,7 +295,6 @@ fun MainScreen(
|
|||
)
|
||||
Modifier.focusRequester(focusRequester)
|
||||
else Modifier)
|
||||
.padding(bottom = 90.dp)
|
||||
.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
|
@ -367,14 +312,13 @@ fun MainScreen(
|
|||
}
|
||||
}
|
||||
},
|
||||
) { innerPadding ->
|
||||
) {
|
||||
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
) {
|
||||
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
|
@ -471,14 +415,57 @@ fun MainScreen(
|
|||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(.90f)
|
||||
.overscroll(ScrollableDefaults.overscrollEffect())
|
||||
.padding(innerPadding),
|
||||
.overscroll(ScrollableDefaults.overscrollEffect()),
|
||||
state = rememberLazyListState(0, uiState.tunnels.count()),
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = true,
|
||||
reverseLayout = false,
|
||||
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(
|
||||
uiState.tunnels,
|
||||
key = { tunnel -> tunnel.id },
|
||||
|
@ -534,7 +521,7 @@ fun MainScreen(
|
|||
(uiState.vpnState.status == Tunnel.State.UP) &&
|
||||
(tunnel.name == uiState.vpnState.name)
|
||||
) {
|
||||
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||
return@RowListItem
|
||||
}
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
|
@ -568,7 +555,7 @@ fun MainScreen(
|
|||
uiState.settings.isAutoTunnelEnabled &&
|
||||
!uiState.settings.isAutoTunnelPaused
|
||||
) {
|
||||
showSnackbarMessage(
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
)
|
||||
} else {
|
||||
|
@ -591,7 +578,7 @@ fun MainScreen(
|
|||
) &&
|
||||
!uiState.settings.isAutoTunnelPaused
|
||||
) {
|
||||
showSnackbarMessage(
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
)
|
||||
} else
|
||||
|
@ -634,7 +621,7 @@ fun MainScreen(
|
|||
IconButton(
|
||||
onClick = {
|
||||
if (uiState.settings.isAutoTunnelEnabled) {
|
||||
showSnackbarMessage(
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
)
|
||||
} else {
|
||||
|
@ -658,7 +645,7 @@ fun MainScreen(
|
|||
) {
|
||||
expanded.value = !expanded.value
|
||||
} else {
|
||||
showSnackbarMessage(
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.TunnelOnAction.message
|
||||
)
|
||||
}
|
||||
|
@ -672,7 +659,7 @@ fun MainScreen(
|
|||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
tunnel.name == uiState.vpnState.name
|
||||
) {
|
||||
showSnackbarMessage(
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.TunnelOffAction.message
|
||||
)
|
||||
} else {
|
||||
|
@ -690,7 +677,7 @@ fun MainScreen(
|
|||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
tunnel.name == uiState.vpnState.name
|
||||
) {
|
||||
showSnackbarMessage(
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.TunnelOffAction.message
|
||||
)
|
||||
} else {
|
||||
|
|
|
@ -122,6 +122,7 @@ constructor(
|
|||
addTunnel(tunnelConfig)
|
||||
Result.Success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Result.Error(Event.Error.InvalidQrCode)
|
||||
}
|
||||
}
|
||||
|
@ -158,6 +159,7 @@ constructor(
|
|||
return Result.Error(Event.Error.InvalidFileExtension)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return Result.Error(Event.Error.FileReadFailed)
|
||||
}
|
||||
}
|
||||
|
@ -249,6 +251,7 @@ constructor(
|
|||
return try {
|
||||
fileName.substring(fileName.lastIndexOf('.'))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
@ -76,6 +75,7 @@ import com.wireguard.android.backend.Tunnel
|
|||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
@OptIn(
|
||||
|
@ -94,9 +95,8 @@ import java.io.File
|
|||
)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
padding: PaddingValues,
|
||||
viewModel: SettingsViewModel = hiltViewModel(),
|
||||
showSnackbarMessage: (String) -> Unit,
|
||||
appViewModel: AppViewModel,
|
||||
focusRequester: FocusRequester
|
||||
) {
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
|
@ -141,9 +141,10 @@ fun SettingsScreen(
|
|||
}
|
||||
FileUtils.saveFilesToZip(context, files)
|
||||
didExportFiles = true
|
||||
showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||
appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||
} 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 {
|
||||
when (it) {
|
||||
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(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding),
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(scrollState),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.LocationOff,
|
||||
|
@ -301,11 +302,11 @@ fun SettingsScreen(
|
|||
},
|
||||
onError = { _ ->
|
||||
showAuthPrompt = false
|
||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
},
|
||||
onFailure = {
|
||||
showAuthPrompt = false
|
||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -314,7 +315,7 @@ fun SettingsScreen(
|
|||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.one_tunnel_required),
|
||||
|
@ -329,7 +330,7 @@ fun SettingsScreen(
|
|||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier.fillMaxSize().padding(padding).verticalScroll(scrollState).clickable(
|
||||
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
|
||||
indication = null,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
|
@ -501,11 +502,11 @@ fun SettingsScreen(
|
|||
) {
|
||||
when (false) {
|
||||
isBackgroundLocationGranted ->
|
||||
showSnackbarMessage(
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Error.BackgroundLocationRequired.message
|
||||
)
|
||||
fineLocationState.status.isGranted ->
|
||||
showSnackbarMessage(
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Error.PreciseLocationRequired.message
|
||||
)
|
||||
viewModel.isLocationEnabled(context) ->
|
||||
|
@ -558,7 +559,7 @@ fun SettingsScreen(
|
|||
onCheckChanged = {
|
||||
viewModel.onToggleKernelMode().let {
|
||||
when (it) {
|
||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
is Result.Success -> {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -183,9 +183,10 @@ constructor(
|
|||
if (!uiState.value.settings.isKernelEnabled) {
|
||||
try {
|
||||
rootShell.start()
|
||||
Timber.d("Root shell accepted!")
|
||||
Timber.i("Root shell accepted!")
|
||||
saveKernelMode(on = true)
|
||||
} catch (e: RootShell.RootShellException) {
|
||||
Timber.e(e)
|
||||
saveKernelMode(on = false)
|
||||
return Result.Error(Event.Error.RootDenied)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
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.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.automirrored.rounded.ArrowForward
|
||||
import androidx.compose.material.icons.rounded.Book
|
||||
import androidx.compose.material.icons.rounded.FormatListNumbered
|
||||
import androidx.compose.material.icons.rounded.Mail
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
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.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
|
||||
@Composable
|
||||
fun SupportScreen(
|
||||
padding: PaddingValues,
|
||||
viewModel: SupportViewModel = hiltViewModel(),
|
||||
showSnackbarMessage: (String) -> Unit,
|
||||
appViewModel: AppViewModel,
|
||||
navController: NavController,
|
||||
focusRequester: FocusRequester
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
@ -67,34 +64,6 @@ fun SupportScreen(
|
|||
|
||||
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) {
|
||||
LoadingScreen()
|
||||
return
|
||||
|
@ -104,9 +73,10 @@ fun SupportScreen(
|
|||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier.fillMaxSize().padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.focusable()
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.focusable()
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
|
@ -115,15 +85,19 @@ fun SupportScreen(
|
|||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
||||
Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 20.dp)
|
||||
})
|
||||
.padding(bottom = 25.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(20.dp)) {
|
||||
val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward
|
||||
Text(
|
||||
stringResource(R.string.thank_you),
|
||||
textAlign = TextAlign.Start,
|
||||
|
@ -138,8 +112,10 @@ fun SupportScreen(
|
|||
modifier = Modifier.padding(bottom = 20.dp),
|
||||
)
|
||||
TextButton(
|
||||
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester),
|
||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||
modifier = Modifier
|
||||
.padding(vertical = 5.dp)
|
||||
.focusRequester(focusRequester),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
|
@ -147,7 +123,8 @@ fun SupportScreen(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
||||
val icon = Icons.Rounded.Book
|
||||
Icon(icon, icon.name)
|
||||
Text(
|
||||
stringResource(id = R.string.docs_description),
|
||||
textAlign = TextAlign.Justify,
|
||||
|
@ -155,8 +132,8 @@ fun SupportScreen(
|
|||
)
|
||||
}
|
||||
Icon(
|
||||
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||
stringResource(id = R.string.go)
|
||||
forwardIcon,
|
||||
forwardIcon.name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -165,7 +142,7 @@ fun SupportScreen(
|
|||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
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),
|
||||
) {
|
||||
Row(
|
||||
|
@ -174,9 +151,10 @@ fun SupportScreen(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
val icon = ImageVector.vectorResource(R.drawable.discord)
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
||||
stringResource(id = R.string.discord),
|
||||
icon,
|
||||
icon.name,
|
||||
Modifier.size(25.dp),
|
||||
)
|
||||
Text(
|
||||
|
@ -186,8 +164,8 @@ fun SupportScreen(
|
|||
)
|
||||
}
|
||||
Icon(
|
||||
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||
stringResource(id = R.string.go)
|
||||
forwardIcon,
|
||||
forwardIcon.name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -196,7 +174,7 @@ fun SupportScreen(
|
|||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
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),
|
||||
) {
|
||||
Row(
|
||||
|
@ -205,20 +183,21 @@ fun SupportScreen(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
val icon = ImageVector.vectorResource(R.drawable.github)
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.github),
|
||||
stringResource(id = R.string.github),
|
||||
imageVector = icon,
|
||||
icon.name,
|
||||
Modifier.size(25.dp),
|
||||
)
|
||||
Text(
|
||||
"Open an issue",
|
||||
stringResource(id = R.string.open_issue),
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||
stringResource(id = R.string.go)
|
||||
forwardIcon,
|
||||
forwardIcon.name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -227,7 +206,7 @@ fun SupportScreen(
|
|||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
TextButton(
|
||||
onClick = { launchEmail() },
|
||||
onClick = { appViewModel.launchEmail() },
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
|
@ -236,13 +215,42 @@ fun SupportScreen(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
||||
val icon = Icons.Rounded.Mail
|
||||
Icon(icon, icon.name)
|
||||
Text(
|
||||
stringResource(id = R.string.email_description),
|
||||
textAlign = TextAlign.Justify,
|
||||
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(
|
||||
Icons.AutoMirrored.Rounded.ArrowForward,
|
||||
stringResource(id = R.string.go)
|
||||
|
@ -258,7 +266,7 @@ fun SupportScreen(
|
|||
fontSize = 16.sp,
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||
},
|
||||
)
|
||||
Row(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,17 +1,22 @@
|
|||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
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 BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes
|
||||
const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1000L // 30 minutes
|
||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
||||
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3000L
|
||||
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
|
||||
const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1_000L // 30 minutes
|
||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
|
||||
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
|
||||
const val TOGGLE_TUNNEL_DELAY = 300L
|
||||
const val CONF_FILE_EXTENSION = ".conf"
|
||||
const val ZIP_FILE_EXTENSION = ".zip"
|
||||
const val URI_CONTENT_SCHEME = "content"
|
||||
const val URI_PACKAGE_SCHEME = "package"
|
||||
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 ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.os.Environment
|
|||
import android.provider.MediaStore
|
||||
import android.provider.MediaStore.MediaColumns
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.util.zip.ZipEntry
|
||||
|
@ -43,6 +44,31 @@ object FileUtils {
|
|||
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>) {
|
||||
val zipOutputStream =
|
||||
createDownloadsFileOutputStream(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -146,10 +146,6 @@
|
|||
<string name="go">go</string>
|
||||
<string name="docs_description">Read the docs (WIP)</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="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>
|
||||
|
@ -176,4 +172,9 @@
|
|||
<string name="excluded">excluded</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="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>
|
|
@ -4,4 +4,5 @@ plugins {
|
|||
alias(libs.plugins.hilt.android) apply false
|
||||
alias(libs.plugins.kotlinxSerialization) apply false
|
||||
alias(libs.plugins.ksp) apply false
|
||||
alias(libs.plugins.androidLibrary) apply false
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ zxingCore = "3.5.3"
|
|||
|
||||
#plugins
|
||||
gradlePlugins-kotlinxSerialization = "1.8.21"
|
||||
material = "1.10.0"
|
||||
|
||||
|
||||
[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-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }
|
||||
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" }
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -41,3 +41,4 @@ fun getLocalProperty(key: String, file: String = "local.properties"): String? {
|
|||
rootProject.name = "WG Tunnel"
|
||||
|
||||
include(":app")
|
||||
include(":logcatter")
|
||||
|
|
Loading…
Reference in New Issue