feat: add logs screen
This commit is contained in:
parent
c0cff297b2
commit
4fc8ffbcbb
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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.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,13 +184,14 @@ class MainActivity : AppCompatActivity() {
|
||||||
return@Scaffold
|
return@Scaffold
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Column(modifier = Modifier.padding(padding)) {
|
||||||
NavHost(navController, startDestination = Screen.Main.route) {
|
NavHost(navController, startDestination = Screen.Main.route) {
|
||||||
composable(
|
composable(
|
||||||
Screen.Main.route,
|
Screen.Main.route,
|
||||||
) {
|
) {
|
||||||
MainScreen(
|
MainScreen(
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
appViewModel = appViewModel,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -187,31 +199,29 @@ class MainActivity : AppCompatActivity() {
|
||||||
Screen.Settings.route,
|
Screen.Settings.route,
|
||||||
) {
|
) {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
padding = padding,
|
appViewModel = appViewModel,
|
||||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
)
|
)
|
||||||
//
|
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
Screen.Support.route,
|
Screen.Support.route,
|
||||||
) {
|
) {
|
||||||
SupportScreen(
|
SupportScreen(
|
||||||
padding = padding,
|
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
appViewModel = appViewModel,
|
||||||
|
navController = navController
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
composable(Screen.Support.Logs.route,) {
|
||||||
|
LogsScreen()
|
||||||
|
}
|
||||||
composable("${Screen.Config.route}/{id}") {
|
composable("${Screen.Config.route}/{id}") {
|
||||||
val id = it.arguments?.getString("id")
|
val id = it.arguments?.getString("id")
|
||||||
if (!id.isNullOrBlank()) {
|
if (!id.isNullOrBlank()) {
|
||||||
ConfigScreen(
|
ConfigScreen(
|
||||||
padding = padding,
|
|
||||||
navController = navController,
|
navController = navController,
|
||||||
id = id,
|
id = id,
|
||||||
showSnackbarMessage = { message ->
|
appViewModel = appViewModel,
|
||||||
showSnackBarMessage(message)
|
|
||||||
},
|
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -221,4 +231,5 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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.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,
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,7 +73,8 @@ fun SupportScreen(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxSize().padding(padding)
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.focusable()
|
.focusable()
|
||||||
) {
|
) {
|
||||||
|
@ -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
|
||||||
|
.height(IntrinsicSize.Min)
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
.padding(top = 10.dp)
|
.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(
|
||||||
|
|
|
@ -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
|
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"
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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="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>
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,6 +85,7 @@ 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" }
|
||||||
|
@ -91,3 +93,4 @@ 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" }
|
|
@ -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"
|
rootProject.name = "WG Tunnel"
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
include(":logcatter")
|
||||||
|
|
Loading…
Reference in New Issue