Compare commits

..

12 Commits

Author SHA1 Message Date
Roy Orbitson 6e961e0994 Preserve DNS search domains (#344) 2024-09-06 23:01:59 -04:00
Zane Schepke b2a2b9fcf4 fix: android nightly workflow
bump deps
2024-08-27 01:12:34 -04:00
Zane Schepke 543a61efe0 add package build types 2024-08-25 22:34:28 -04:00
Zane Schepke 688fad770c chore: typo fix readme 2024-08-17 23:12:58 -04:00
Zane Schepke e87dd8d3ce chore: update README.md 2024-08-17 23:12:03 -04:00
Zane Schepke 30851a7d7b fix: tile control and kernel sync (#320)
increase auto tunnel delay to 3 seconds

optimize stats job by killing it when app is backgrounded

fix tunnel launch from background

add restart of services and tunnels after update
2024-08-17 21:29:31 -04:00
Zane Schepke 3f4673b2a7 fix: improve navigation animation speed
Fixes possible crashes on slow androidTVs

Closes #49
2024-08-17 00:43:57 -04:00
Zane Schepke 528a1f84e4 fix: minor ui changes 2024-08-16 22:13:31 -04:00
Zane Schepke 1af474c449 bump version code 2024-08-13 17:07:30 -04:00
Zane Schepke 7e3405f3fd fix: location disclosure missing 2024-08-13 16:55:14 -04:00
Zane Schepke ffeb089aa7 Merge branch 'main' of https://github.com/zaneschepke/wgtunnel 2024-08-11 00:32:39 -04:00
Zane Schepke 3838c32ddf remove duplicate language 2024-08-11 00:32:08 -04:00
40 changed files with 446 additions and 318 deletions
+1
View File
@@ -149,6 +149,7 @@ jobs:
run: | run: |
echo "RELEASE_NOTES=Nightly build for the latest development version of the app." >> $GITHUB_ENV echo "RELEASE_NOTES=Nightly build for the latest development version of the app." >> $GITHUB_ENV
gh release delete nightly --yes || true gh release delete nightly --yes || true
git push origin :nightly || true
- name: On prerelease release notes - name: On prerelease release notes
if: ${{ inputs.release_type == 'prerelease' }} if: ${{ inputs.release_type == 'prerelease' }}
+2 -2
View File
@@ -55,13 +55,13 @@ and on while on different networks. This app was created to offer a free solutio
* Split tunneling by application with search * Split tunneling by application with search
* WireGuard support for kernel and userspace modes * WireGuard support for kernel and userspace modes
* Amnezia support for userspace mode for DPI/censorship protection * Amnezia support for userspace mode for DPI/censorship protection
* Pre/Post Up/Down scripts support for all modes on a rooted device
* Always-On VPN support * Always-On VPN support
* Export Amnezia and WireGuard tunnels to zip * Export Amnezia and WireGuard tunnels to zip
* Quick tile support for tunnel toggling, auto-tunneling * Quick tile support for tunnel toggling, auto-tunneling
* Static shortcuts support for tunnel toggling, auto-tunneling * Static shortcuts support for tunnel toggling, auto-tunneling
* Intent automation support for all tunnels * Intent automation support for all tunnels
* Automatic auto-tunneling service restart after reboot * Automatic auto-tunneling service and/or tunnel restart after reboot or app update
* Automatic tunnel restart after reboot
* Battery preservation measures * Battery preservation measures
* Restart tunnel on ping failure (beta) * Restart tunnel on ping failure (beta)
+12 -1
View File
@@ -58,14 +58,25 @@ android {
) )
signingConfig = signingConfigs.getByName(Constants.RELEASE) signingConfig = signingConfigs.getByName(Constants.RELEASE)
} }
debug { isDebuggable = true } debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
resValue("string", "app_name", "WG Tunnel - Debug")
isDebuggable = true
}
create(Constants.PRERELEASE) { create(Constants.PRERELEASE) {
initWith(buildTypes.getByName(Constants.RELEASE)) initWith(buildTypes.getByName(Constants.RELEASE))
applicationIdSuffix = ".prerelease"
versionNameSuffix = "-pre"
resValue("string", "app_name", "WG Tunnel - Pre")
} }
create(Constants.NIGHTLY) { create(Constants.NIGHTLY) {
initWith(buildTypes.getByName(Constants.RELEASE)) initWith(buildTypes.getByName(Constants.RELEASE))
applicationIdSuffix = ".nightly"
versionNameSuffix = "-nightly"
resValue("string", "app_name", "WG Tunnel - Nightly")
} }
applicationVariants.all { applicationVariants.all {
+17
View File
@@ -167,6 +167,16 @@
android:stopWithTask="false" android:stopWithTask="false"
tools:node="merge" /> tools:node="merge" />
<service
android:name=".service.foreground.TunnelBackgroundService"
android:exported="false"
android:foregroundServiceType="systemExempted"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<receiver <receiver
android:name=".receiver.BootReceiver" android:name=".receiver.BootReceiver"
android:enabled="true" android:enabled="true"
@@ -184,6 +194,13 @@
android:name=".receiver.BackgroundActionReceiver" android:name=".receiver.BackgroundActionReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"/> android:exported="false"/>
<receiver
android:name=".receiver.AppUpdateReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver <receiver
android:name=".receiver.KernelReceiver" android:name=".receiver.KernelReceiver"
android:exported="false" android:exported="false"
@@ -3,10 +3,15 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application import android.app.Application
import android.os.StrictMode import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy import android.os.StrictMode.ThreadPolicy
import com.zaneschepke.logcatter.LocalLogCollector
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -17,6 +22,13 @@ class WireGuardAutoTunnel : Application() {
@ApplicationScope @ApplicationScope
lateinit var applicationScope: CoroutineScope lateinit var applicationScope: CoroutineScope
@Inject
lateinit var localLogCollector: LocalLogCollector
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
@@ -33,6 +45,11 @@ class WireGuardAutoTunnel : Application() {
} else { } else {
Timber.plant(ReleaseTree()) Timber.plant(ReleaseTree())
} }
if (!isRunningOnTv()) {
applicationScope.launch(ioDispatcher) {
localLogCollector.start()
}
}
} }
companion object { companion object {
@@ -0,0 +1,47 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class AppUpdateReceiver : BroadcastReceiver() {
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelService: TunnelService
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
applicationScope.launch {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) {
Timber.i("Restarting services after upgrade")
serviceManager.startWatcherServiceForeground(context)
}
if (!settings.isAutoTunnelEnabled || settings.isAutoTunnelPaused) {
val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive }
if (tunnels.isNotEmpty()) context.startTunnelBackground(tunnels.first().id)
}
}
}
}
@@ -5,10 +5,12 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@@ -25,14 +27,19 @@ class BackgroundActionReceiver : BroadcastReceiver() {
@Inject @Inject
lateinit var tunnelConfigRepository: TunnelConfigRepository lateinit var tunnelConfigRepository: TunnelConfigRepository
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val id = intent.getIntExtra(TUNNEL_ID_EXTRA_KEY, 0) val id = intent.getIntExtra(TUNNEL_ID_EXTRA_KEY, 0)
if (id == 0) return if (id == 0) return
when (intent.action) { when (intent.action) {
ACTION_CONNECT -> { ACTION_CONNECT -> {
Timber.d("Connect actions")
applicationScope.launch { applicationScope.launch {
val tunnel = tunnelConfigRepository.getById(id) val tunnel = tunnelConfigRepository.getById(id)
tunnel?.let { tunnel?.let {
serviceManager.startTunnelBackgroundService(context)
tunnelService.get().startTunnel(it) tunnelService.get().startTunnel(it)
} }
} }
@@ -41,6 +48,7 @@ class BackgroundActionReceiver : BroadcastReceiver() {
applicationScope.launch { applicationScope.launch {
val tunnel = tunnelConfigRepository.getById(id) val tunnel = tunnelConfigRepository.getById(id)
tunnel?.let { tunnel?.let {
serviceManager.stopTunnelBackgroundService(context)
tunnelService.get().stopTunnel(it) tunnelService.get().stopTunnel(it)
} }
} }
@@ -7,6 +7,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -35,7 +36,7 @@ class BootReceiver : BroadcastReceiver() {
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
if (settings.isRestoreOnBootEnabled) { if (settings.isRestoreOnBootEnabled) {
appDataRepository.getStartTunnelConfig()?.let { appDataRepository.getStartTunnelConfig()?.let {
tunnelService.get().startTunnel(it) context.startTunnelBackground(it.id)
} }
} }
if (settings.isAutoTunnelEnabled) { if (settings.isAutoTunnelEnabled) {
@@ -50,4 +50,20 @@ class ServiceManager {
AutoTunnelService::class.java, AutoTunnelService::class.java,
) )
} }
fun startTunnelBackgroundService(context: Context) {
actionOnService(
Action.START_FOREGROUND,
context,
TunnelBackgroundService::class.java,
)
}
fun stopTunnelBackgroundService(context: Context) {
actionOnService(
Action.STOP,
context,
TunnelBackgroundService::class.java,
)
}
} }
@@ -0,0 +1,41 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Notification
import android.os.Bundle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class TunnelBackgroundService : ForegroundService() {
@Inject
lateinit var notificationService: NotificationService
private val foregroundId = 123
override fun onCreate() {
super.onCreate()
startForeground(foregroundId, createNotification())
}
override fun startService(extras: Bundle?) {
super.startService(extras)
startForeground(foregroundId, createNotification())
}
override fun stopService() {
super.stopService()
stopForeground(STOP_FOREGROUND_REMOVE)
}
private fun createNotification(): Notification {
return notificationService.createNotification(
getString(R.string.vpn_channel_id),
getString(R.string.vpn_channel_name),
getString(R.string.tunnel_start_text),
description = "",
)
}
}
@@ -7,6 +7,8 @@ import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.Action import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.AutoTunnelService import com.zaneschepke.wireguardautotunnel.service.foreground.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import com.zaneschepke.wireguardautotunnel.util.extensions.stopTunnelBackground
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -42,8 +44,8 @@ class ShortcutsActivity : ComponentActivity() {
Timber.d("Shortcut action on name: ${tunnelConfig?.name}") Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelConfig?.let { tunnelConfig?.let {
when (intent.action) { when (intent.action) {
Action.START.name -> tunnelService.get().startTunnel(it) Action.START.name -> this@ShortcutsActivity.startTunnelBackground(it.id)
Action.STOP.name -> tunnelService.get().stopTunnel(it) Action.STOP.name -> this@ShortcutsActivity.stopTunnelBackground(it.id)
else -> Unit else -> Unit
} }
} }
@@ -68,6 +68,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
Timber.d("Click")
lifecycleScope.launch { lifecycleScope.launch {
val context = this@TunnelControlTile val context = this@TunnelControlTile
val lastActive = appDataRepository.getStartTunnelConfig() val lastActive = appDataRepository.getStartTunnelConfig()
@@ -14,4 +14,7 @@ interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun runningTunnelNames(): Set<String> suspend fun runningTunnelNames(): Set<String>
suspend fun getState(): TunnelState suspend fun getState(): TunnelState
fun cancelStatsJob()
fun startStatsJob()
} }
@@ -14,7 +14,6 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStati
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -56,8 +55,17 @@ constructor(
return runCatching { return runCatching {
when (val backend = backend()) { when (val backend = backend()) {
is Backend -> backend.setState(this, tunnelState.toWgState(), TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick)).let { TunnelState.from(it) } is Backend -> backend.setState(this, tunnelState.toWgState(), TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick)).let { TunnelState.from(it) }
is org.amnezia.awg.backend.Backend -> backend.setState(this, tunnelState.toAmState(), TunnelConfig.configFromAmQuick(tunnelConfig.amQuick)).let { is org.amnezia.awg.backend.Backend -> {
TunnelState.from(it) val config = if (tunnelConfig.amQuick.isBlank()) {
TunnelConfig.configFromAmQuick(
tunnelConfig.wgQuick,
)
} else {
TunnelConfig.configFromAmQuick(tunnelConfig.amQuick)
}
backend.setState(this, tunnelState.toAmState(), config).let {
TunnelState.from(it)
}
} }
else -> throw NotImplementedError() else -> throw NotImplementedError()
} }
@@ -144,6 +152,14 @@ constructor(
} }
} }
override fun cancelStatsJob() {
statsJob?.cancel()
}
override fun startStatsJob() {
statsJob = startTunnelStatisticsJob()
}
override fun getName(): String { override fun getName(): String {
return _vpnState.value.tunnelConfig?.name ?: "" return _vpnState.value.tunnelConfig?.name ?: ""
} }
@@ -155,15 +171,9 @@ constructor(
private fun handleStateChange(state: TunnelState) { private fun handleStateChange(state: TunnelState) {
emitTunnelState(state) emitTunnelState(state)
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate() WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
if (state == TunnelState.UP) { when (state) {
statsJob = startTunnelStatisticsJob() TunnelState.UP -> startStatsJob()
} else -> cancelStatsJob()
if (state == TunnelState.DOWN) {
try {
statsJob?.cancel()
} catch (e: CancellationException) {
Timber.i("Stats job cancelled")
}
} }
} }
@@ -5,6 +5,9 @@ import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -15,6 +18,7 @@ import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface
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.getValue
@@ -35,8 +39,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
@@ -48,6 +51,7 @@ 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.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.StringValue import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -60,10 +64,7 @@ class MainActivity : AppCompatActivity() {
lateinit var appStateRepository: AppStateRepository lateinit var appStateRepository: AppStateRepository
@Inject @Inject
lateinit var settingsRepository: SettingsRepository lateinit var tunnelService: TunnelService
@Inject
lateinit var serviceManager: ServiceManager
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -72,13 +73,6 @@ class MainActivity : AppCompatActivity() {
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb())) enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
lifecycleScope.launch {
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
serviceManager.startWatcherService(application.applicationContext)
}
}
setContent { setContent {
val appViewModel = hiltViewModel<AppViewModel>() val appViewModel = hiltViewModel<AppViewModel>()
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle() val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
@@ -127,7 +121,7 @@ class MainActivity : AppCompatActivity() {
) )
} }
}, },
// TODO refactor containerColor = MaterialTheme.colorScheme.background,
modifier = modifier =
Modifier Modifier
.focusable() .focusable()
@@ -148,92 +142,97 @@ class MainActivity : AppCompatActivity() {
) )
}, },
) { padding -> ) { padding ->
NavHost( Surface(modifier = Modifier.fillMaxSize().padding(padding)) {
navController, NavHost(
startDestination = (if (isPinLockEnabled == true) Screen.Lock.route else Screen.Main.route), navController,
modifier = enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
Modifier exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
.padding(padding) startDestination = (if (isPinLockEnabled == true) Screen.Lock.route else Screen.Main.route),
.fillMaxSize(),
) {
composable(
Screen.Main.route,
) { ) {
MainScreen( composable(
focusRequester = focusRequester, Screen.Main.route,
appViewModel = appViewModel, ) {
navController = navController, MainScreen(
)
}
composable(
Screen.Settings.route,
) {
SettingsScreen(
appViewModel = appViewModel,
navController = navController,
focusRequester = focusRequester,
)
}
composable(
Screen.Support.route,
) {
SupportScreen(
focusRequester = focusRequester,
navController = navController,
)
}
composable(Screen.Support.Logs.route) {
LogsScreen()
}
composable(
"${Screen.Config.route}/{id}?configType={configType}",
arguments =
listOf(
navArgument("id") {
type = NavType.StringType
defaultValue = "0"
},
navArgument("configType") {
type = NavType.StringType
defaultValue = ConfigType.WIREGUARD.name
},
),
) {
val id = it.arguments?.getString("id")
val configType =
ConfigType.valueOf(
it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name,
)
if (!id.isNullOrBlank()) {
ConfigScreen(
navController = navController,
tunnelId = id,
appViewModel = appViewModel,
focusRequester = focusRequester, focusRequester = focusRequester,
configType = configType, appViewModel = appViewModel,
navController = navController,
) )
} }
} composable(
composable("${Screen.Option.route}/{id}") { Screen.Settings.route,
val id = it.arguments?.getString("id") ) {
if (!id.isNullOrBlank()) { SettingsScreen(
OptionsScreen(
navController = navController,
tunnelId = id,
appViewModel = appViewModel, appViewModel = appViewModel,
navController = navController,
focusRequester = focusRequester, focusRequester = focusRequester,
) )
} }
} composable(
composable(Screen.Lock.route) { Screen.Support.route,
PinLockScreen( ) {
navController = navController, SupportScreen(
appViewModel = appViewModel, focusRequester = focusRequester,
) navController = navController,
)
}
composable(Screen.Support.Logs.route) {
LogsScreen()
}
composable(
"${Screen.Config.route}/{id}?configType={configType}",
arguments =
listOf(
navArgument("id") {
type = NavType.StringType
defaultValue = "0"
},
navArgument("configType") {
type = NavType.StringType
defaultValue = ConfigType.WIREGUARD.name
},
),
) {
val id = it.arguments?.getString("id")
val configType =
ConfigType.valueOf(
it.arguments?.getString("configType") ?: ConfigType.WIREGUARD.name,
)
if (!id.isNullOrBlank()) {
ConfigScreen(
navController = navController,
tunnelId = id,
appViewModel = appViewModel,
focusRequester = focusRequester,
configType = configType,
)
}
}
composable("${Screen.Option.route}/{id}") {
val id = it.arguments?.getString("id")
if (!id.isNullOrBlank()) {
OptionsScreen(
navController = navController,
tunnelId = id,
appViewModel = appViewModel,
focusRequester = focusRequester,
)
}
}
composable(Screen.Lock.route) {
PinLockScreen(
navController = navController,
appViewModel = appViewModel,
)
}
} }
} }
} }
} }
} }
} }
override fun onDestroy() {
super.onDestroy()
tunnelService.cancelStatsJob()
}
} }
@@ -9,21 +9,16 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import com.zaneschepke.logcatter.LocalLogCollector
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@@ -41,11 +36,7 @@ class SplashActivity : ComponentActivity() {
lateinit var tunnelService: Provider<TunnelService> lateinit var tunnelService: Provider<TunnelService>
@Inject @Inject
lateinit var localLogCollector: LocalLogCollector lateinit var serviceManager: ServiceManager
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -54,29 +45,17 @@ class SplashActivity : ComponentActivity() {
} }
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
applicationScope.launch {
if (!this@SplashActivity.isRunningOnTv()) localLogCollector.start()
}
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) { repeatOnLifecycle(Lifecycle.State.CREATED) {
val pinLockEnabled = appStateRepository.isPinLockEnabled() val pinLockEnabled = appStateRepository.isPinLockEnabled()
if (pinLockEnabled) { if (pinLockEnabled) {
PinManager.initialize(WireGuardAutoTunnel.instance) PinManager.initialize(WireGuardAutoTunnel.instance)
} }
// TODO eventually make this support multi-tunnel
Timber.d("Check for active tunnels")
val settings = appDataRepository.settings.getSettings() val settings = appDataRepository.settings.getSettings()
if (settings.isKernelEnabled) { if (settings.isAutoTunnelEnabled) serviceManager.startWatcherService(application.applicationContext)
// delay in case state change is underway while app is opened if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startStatsJob()
delay(Constants.FOCUS_REQUEST_DELAY) val tunnels = appDataRepository.tunnels.getActive()
val activeTunnels = appDataRepository.tunnels.getActive() if (tunnels.isNotEmpty() && tunnelService.get().getState() == TunnelState.DOWN) tunnelService.get().startTunnel(tunnels.first())
Timber.d("Kernel mode enabled, seeing if we need to start a tunnel")
activeTunnels.firstOrNull()?.let {
Timber.d("Trying to start active kernel tunnel: ${it.name}")
tunnelService.get().startTunnel(it)
}
}
requestTunnelTileServiceStateUpdate() requestTunnelTileServiceStateUpdate()
requestAutoTunnelTileServiceUpdate() requestAutoTunnelTileServiceUpdate()
@@ -10,37 +10,29 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import com.zaneschepke.wireguardautotunnel.ui.Screen
@Composable @Composable
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) { fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
val backStackEntry = navController.currentBackStackEntryAsState()
var showBottomBar by rememberSaveable { mutableStateOf(true) } var showBottomBar by rememberSaveable { mutableStateOf(true) }
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
// TODO find a better way to hide nav bar showBottomBar = bottomNavItems.firstOrNull { navBackStackEntry?.destination?.route?.contains(it.route) == true } != null
showBottomBar =
when (navBackStackEntry?.destination?.route) {
Screen.Lock.route -> false
else -> true
}
NavigationBar( if (showBottomBar) {
containerColor = if (!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background, NavigationBar(
) { containerColor = MaterialTheme.colorScheme.surface,
if (showBottomBar) { ) {
bottomNavItems.forEach { item -> bottomNavItems.forEach { item ->
val selected = item.route == backStackEntry.value?.destination?.route val selected = navBackStackEntry?.destination?.route?.contains(item.route) == true
NavigationBarItem( NavigationBarItem(
selected = selected, selected = selected,
onClick = { onClick = {
if (navBackStackEntry?.destination?.route == item.route) return@NavigationBarItem
navController.navigate(item.route) { navController.navigate(item.route) {
// Pop up to the start destination of the graph to // Pop up to the start destination of the graph to
// avoid building up a large stack of destinations // avoid building up a large stack of destinations
@@ -110,7 +110,12 @@ fun ConfigScreen(
LaunchedEffect(uiState.loading) { LaunchedEffect(uiState.loading) {
if (!uiState.loading && context.isRunningOnTv()) { if (!uiState.loading && context.isRunningOnTv()) {
delay(Constants.FOCUS_REQUEST_DELAY) delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus() kotlin.runCatching {
focusRequester.requestFocus()
}.onFailure {
delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus()
}
} }
} }
@@ -296,7 +296,7 @@ constructor(
val wgQuick = buildConfig().toWgQuickString(true) val wgQuick = buildConfig().toWgQuickString(true)
val amQuick = val amQuick =
if (configType == ConfigType.AMNEZIA) { if (configType == ConfigType.AMNEZIA) {
buildAmConfig().toAwgQuickString() buildAmConfig().toAwgQuickString(true)
} else { } else {
TunnelConfig.AM_QUICK_DEFAULT TunnelConfig.AM_QUICK_DEFAULT
} }
@@ -25,7 +25,10 @@ data class InterfaceProxy(
publicKey = i.keyPair.publicKey.toBase64().trim(), publicKey = i.keyPair.publicKey.toBase64().trim(),
privateKey = i.keyPair.privateKey.toBase64().trim(), privateKey = i.keyPair.privateKey.toBase64().trim(),
addresses = i.addresses.joinToString(", ").trim(), addresses = i.addresses.joinToString(", ").trim(),
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(), dnsServers = listOf(
i.dnsServers.joinToString(", ").replace("/", "").trim(),
i.dnsSearchDomains.joinToString(", ").trim(),
).filter { it.length > 0 } .joinToString(", "),
listenPort = listenPort =
if (i.listenPort.isPresent) { if (i.listenPort.isPresent) {
i.listenPort.get().toString().trim() i.listenPort.get().toString().trim()
@@ -4,9 +4,6 @@ import android.annotation.SuppressLint
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.ScrollableDefaults
@@ -147,7 +144,12 @@ fun MainScreen(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (context.isRunningOnTv()) { if (context.isRunningOnTv()) {
delay(Constants.FOCUS_REQUEST_DELAY) delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus() kotlin.runCatching {
focusRequester.requestFocus()
}.onFailure {
delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus()
}
} }
} }
@@ -265,12 +267,8 @@ fun MainScreen(
reverseLayout = false, reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(), flingBehavior = ScrollableDefaults.flingBehavior(),
) { ) {
item { if (uiState.tunnels.isEmpty()) {
AnimatedVisibility( item {
uiState.tunnels.isEmpty(),
exit = fadeOut(),
enter = fadeIn(),
) {
GettingStartedLabel(onClick = { context.openWebUrl(it) }) GettingStartedLabel(onClick = { context.openWebUrl(it) })
} }
} }
@@ -179,7 +179,7 @@ constructor(
when (type) { when (type) {
ConfigType.AMNEZIA -> { ConfigType.AMNEZIA -> {
val config = org.amnezia.awg.config.Config.parse(it) val config = org.amnezia.awg.config.Config.parse(it)
amQuick = config.toAwgQuickString() amQuick = config.toAwgQuickString(true)
config.toWgQuickString() config.toWgQuickString()
} }
@@ -252,7 +252,7 @@ constructor(
org.amnezia.awg.config.Config.parse( org.amnezia.awg.config.Config.parse(
zip, zip,
) )
amQuick = config.toAwgQuickString() amQuick = config.toAwgQuickString(true)
config.toWgQuickString() config.toWgQuickString()
} }
@@ -88,7 +88,12 @@ fun OptionsScreen(
optionsViewModel.init(tunnelId) optionsViewModel.init(tunnelId)
if (context.isRunningOnTv()) { if (context.isRunningOnTv()) {
delay(Constants.FOCUS_REQUEST_DELAY) delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus() kotlin.runCatching {
focusRequester.requestFocus()
}.onFailure {
delay(Constants.FOCUS_REQUEST_DELAY)
focusRequester.requestFocus()
}
} }
} }
@@ -12,7 +12,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -67,7 +66,6 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen import com.zaneschepke.wireguardautotunnel.ui.Screen
@@ -84,9 +82,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager import xyz.teamgravity.pin_lock_compose.PinManager
import java.io.File
@OptIn( @OptIn(
ExperimentalPermissionsApi::class, ExperimentalPermissionsApi::class,
@@ -155,43 +151,6 @@ fun SettingsScreen(
}, },
) )
fun exportAllConfigs() {
try {
val wgFiles =
uiState.tunnels.map { config ->
val file = File(context.cacheDir, "${config.name}-wg.conf")
file.outputStream().use {
it.write(config.wgQuick.toByteArray())
}
file
}
val amFiles =
uiState.tunnels.mapNotNull { config ->
if (config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) {
val file = File(context.cacheDir, "${config.name}-am.conf")
file.outputStream().use {
it.write(config.amQuick.toByteArray())
}
file
} else {
null
}
}
scope.launch {
viewModel.onExportTunnels(wgFiles + amFiles).onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}.onSuccess {
didExportFiles = true
appViewModel.showSnackbarMessage(
context.getString(R.string.exported_configs_message),
)
}
}
} catch (e: Exception) {
Timber.e(e)
}
}
fun isBatteryOptimizationsDisabled(): Boolean { fun isBatteryOptimizationsDisabled(): Boolean {
val pm = context.getSystemService(POWER_SERVICE) as PowerManager val pm = context.getSystemService(POWER_SERVICE) as PowerManager
return pm.isIgnoringBatteryOptimizations(context.packageName) return pm.isIgnoringBatteryOptimizations(context.packageName)
@@ -269,17 +228,18 @@ fun SettingsScreen(
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
checkFineLocationGranted() checkFineLocationGranted()
} }
if (!uiState.isLocationDisclosureShown) {
BackgroundLocationDisclosure( BackgroundLocationDisclosure(
!uiState.isLocationDisclosureShown, onDismiss = { viewModel.setLocationDisclosureShown() },
onDismiss = { viewModel.setLocationDisclosureShown() }, onAttest = {
onAttest = { context.launchAppSettings()
context.launchAppSettings() viewModel.setLocationDisclosureShown()
viewModel.setLocationDisclosureShown() },
}, scrollState,
scrollState, focusRequester,
focusRequester, )
) return
}
BackgroundLocationDialog( BackgroundLocationDialog(
showLocationDialog, showLocationDialog,
@@ -299,7 +259,13 @@ fun SettingsScreen(
AuthorizationPrompt( AuthorizationPrompt(
onSuccess = { onSuccess = {
showAuthPrompt = false showAuthPrompt = false
exportAllConfigs() scope.launch {
viewModel.exportAllConfigs().onSuccess {
appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message))
}.onFailure {
appViewModel.showSnackbarMessage(context.getString(R.string.export_configs_failed))
}
}
}, },
onError = { _ -> onError = { _ ->
showAuthPrompt = false showAuthPrompt = false
@@ -378,7 +344,7 @@ fun SettingsScreen(
.focusRequester(focusRequester) .focusRequester(focusRequester)
}, },
) )
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) { if (uiState.settings.isTunnelOnWifiEnabled) {
Column { Column {
FlowRow( FlowRow(
modifier = modifier =
@@ -628,7 +594,7 @@ fun SettingsScreen(
Modifier Modifier
.fillMaxWidth(fillMaxWidth) .fillMaxWidth(fillMaxWidth)
.padding(vertical = 10.dp) .padding(vertical = 10.dp)
.padding(bottom = 140.dp), .padding(bottom = 10.dp),
) { ) {
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
@@ -248,4 +248,13 @@ constructor(
onSuccess() onSuccess()
} }
} }
suspend fun exportAllConfigs(): Result<Unit> {
return kotlin.runCatching {
val tunnels = appDataRepository.tunnels.getAll()
val wgFiles = fileUtils.createWgFiles(tunnels)
val amFiles = fileUtils.createAmFiles(tunnels)
onExportTunnels(wgFiles + amFiles)
}
}
} }
@@ -28,68 +28,60 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
@Composable @Composable
fun BackgroundLocationDisclosure( fun BackgroundLocationDisclosure(onDismiss: () -> Unit, onAttest: () -> Unit, scrollState: ScrollState, focusRequester: FocusRequester) {
show: Boolean,
onDismiss: () -> Unit,
onAttest: () -> Unit,
scrollState: ScrollState,
focusRequester: FocusRequester,
) {
val context = LocalContext.current val context = LocalContext.current
if (show) { Column(
Column( horizontalAlignment = Alignment.CenterHorizontally,
horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top,
verticalArrangement = Arrangement.Top, modifier =
Modifier
.fillMaxSize()
.verticalScroll(scrollState),
) {
Icon(
Icons.Rounded.LocationOff,
contentDescription = stringResource(id = R.string.map),
modifier = modifier =
Modifier Modifier
.fillMaxSize() .padding(30.dp)
.verticalScroll(scrollState), .size(128.dp),
) { )
Icon( Text(
Icons.Rounded.LocationOff, stringResource(R.string.prominent_background_location_title),
contentDescription = stringResource(id = R.string.map), textAlign = TextAlign.Center,
modifier = modifier = Modifier.padding(30.dp),
fontSize = 20.sp,
)
Text(
stringResource(R.string.prominent_background_location_message),
textAlign = TextAlign.Center,
modifier = Modifier.padding(30.dp),
fontSize = 15.sp,
)
Row(
modifier =
if (context.isRunningOnTv()) {
Modifier Modifier
.fillMaxWidth()
.padding(10.dp)
} else {
Modifier
.fillMaxWidth()
.padding(30.dp) .padding(30.dp)
.size(128.dp), },
) verticalAlignment = Alignment.CenterVertically,
Text( horizontalArrangement = Arrangement.SpaceEvenly,
stringResource(R.string.prominent_background_location_title), ) {
textAlign = TextAlign.Center, TextButton(onClick = { onDismiss() }) {
modifier = Modifier.padding(30.dp), Text(stringResource(id = R.string.no_thanks))
fontSize = 20.sp, }
) TextButton(
Text( modifier = Modifier.focusRequester(focusRequester),
stringResource(R.string.prominent_background_location_message), onClick = {
textAlign = TextAlign.Center, onAttest()
modifier = Modifier.padding(30.dp),
fontSize = 15.sp,
)
Row(
modifier =
if (context.isRunningOnTv()) {
Modifier
.fillMaxWidth()
.padding(10.dp)
} else {
Modifier
.fillMaxWidth()
.padding(30.dp)
}, },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
TextButton(onClick = { onDismiss() }) { Text(stringResource(id = R.string.turn_on))
Text(stringResource(id = R.string.no_thanks))
}
TextButton(
modifier = Modifier.focusRequester(focusRequester),
onClick = {
onAttest()
},
) {
Text(stringResource(id = R.string.turn_on))
}
} }
} }
} }
@@ -20,9 +20,10 @@ private val DarkColorScheme =
darkColorScheme( darkColorScheme(
// primary = Purple80, // primary = Purple80,
primary = virdigris, primary = virdigris,
secondary = virdigris, secondary = PurpleGrey40,
// secondary = PurpleGrey80, // secondary = PurpleGrey80,
tertiary = virdigris, tertiary = Pink40,
surfaceTint = Pink80,
// tertiary = Pink80 // tertiary = Pink80
) )
@@ -31,6 +32,7 @@ private val LightColorScheme =
primary = Purple40, primary = Purple40,
secondary = PurpleGrey40, secondary = PurpleGrey40,
tertiary = Pink40, tertiary = Pink40,
surfaceTint = Pink80,
/* Other default colors to override /* Other default colors to override
background = Color(0xFFFFFBFE), background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE), surface = Color(0xFFFFFBFE),
@@ -7,7 +7,7 @@ object Constants {
const val MANUAL_TUNNEL_CONFIG_ID = "0" const val MANUAL_TUNNEL_CONFIG_ID = "0"
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
const val WATCHER_COLLECTION_DELAY = 1_000L const val WATCHER_COLLECTION_DELAY = 3_000L
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"
@@ -24,6 +24,8 @@ object Constants {
const val SUBSCRIPTION_TIMEOUT = 5_000L const val SUBSCRIPTION_TIMEOUT = 5_000L
const val FOCUS_REQUEST_DELAY = 500L const val FOCUS_REQUEST_DELAY = 500L
const val TRANSITION_ANIMATION_TIME = 200
const val DEFAULT_PING_IP = "1.1.1.1" const val DEFAULT_PING_IP = "1.1.1.1"
const val PING_TIMEOUT = 5_000L const val PING_TIMEOUT = 5_000L
const val VPN_RESTART_DELAY = 1_000L const val VPN_RESTART_DELAY = 1_000L
@@ -6,6 +6,8 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.MediaStore.MediaColumns import android.provider.MediaStore.MediaColumns
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
@@ -39,6 +41,26 @@ class FileUtils(
} }
} }
fun createWgFiles(tunnels: TunnelConfigs): List<File> {
return tunnels.map { config ->
val file = File(context.cacheDir, "${config.name}-wg.conf")
file.outputStream().use {
it.write(config.wgQuick.toByteArray())
}
file
}
}
fun createAmFiles(tunnels: TunnelConfigs): List<File> {
return tunnels.filter { it.amQuick != TunnelConfig.AM_QUICK_DEFAULT }.map { config ->
val file = File(context.cacheDir, "${config.name}-am.conf")
file.outputStream().use {
it.write(config.amQuick.toByteArray())
}
file
}
}
suspend fun saveByteArrayToDownloads(content: ByteArray, fileName: String): Result<Unit> { suspend fun saveByteArrayToDownloads(content: ByteArray, fileName: String): Result<Unit> {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
try { try {
@@ -29,7 +29,7 @@ fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
} }
fun Config.toWgQuickString(): String { fun Config.toWgQuickString(): String {
val amQuick = toAwgQuickString() val amQuick = toAwgQuickString(true)
val lines = amQuick.lines().toMutableList() val lines = amQuick.lines().toMutableList()
val linesIterator = lines.iterator() val linesIterator = lines.iterator()
while (linesIterator.hasNext()) { while (linesIterator.hasNext()) {
+2 -2
View File
@@ -1,7 +1,7 @@
object Constants { object Constants {
const val VERSION_NAME = "3.5.0" const val VERSION_NAME = "3.5.1"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 35000 const val VERSION_CODE = 35102
const val TARGET_SDK = 34 const val TARGET_SDK = 34
const val MIN_SDK = 26 const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel" const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -0,0 +1,5 @@
What's new:
- Fixes for tunnels not launching from background
- Add support for restart services after update
- UI animation speed improvements
- Other optimizations
@@ -1,3 +0,0 @@
Melhorias:
- Corrige o bug de permissões do Android 9
- Outras otimizações
@@ -1,5 +0,0 @@
Melhorias:
- Adicionada estatísticas do túnel na tela principal
- Melhoria de navegação de configurações na tela do AndroidTV
- Removida a vibração nas notificações
- Outras correções de bugs
@@ -1,14 +0,0 @@
Recursos
- Adiciona túneis por arquivos .conf, zip, manualmente ou por código QR
- Auto connecta à VPN baseado no nome (SSID) do Wi-Fi, ethernet ou dados móveis
- Túnel dividido por aplicativo com busca
- Suporte à WireGuard em modo kernel ou usuário
- Suporte à Amnezia em modo usuário para proteção contra censura e DPI (Inspeção Profunda de Pacote)
- Suporte à VPN sempre ligada
- Exportação de túneis Amnezia e WireGuard em arquivos zip
- Suporte à quick tile para ligar e desligar a VPN
- Atalhos para o túnel principal para integração com automações
- Intent automation para todos os túneis
- Início automático depois de reiniciar o aparelho
- Medidas para economia de bateria
@@ -1 +0,0 @@
Um cliente de VPN alternativo para WireGuard com recursos adicionais
-1
View File
@@ -1 +0,0 @@
WG Tunnel
+6 -6
View File
@@ -1,7 +1,7 @@
[versions] [versions]
accompanist = "0.34.0" accompanist = "0.34.0"
activityCompose = "1.9.1" activityCompose = "1.9.1"
amneziawgAndroid = "1.2.1" amneziawgAndroid = "1.2.2"
androidx-junit = "1.2.1" androidx-junit = "1.2.1"
appcompat = "1.7.0" appcompat = "1.7.0"
biometricKtx = "1.2.0-alpha05" biometricKtx = "1.2.0-alpha05"
@@ -16,16 +16,16 @@ junit = "4.13.2"
kotlinx-serialization-json = "1.7.1" kotlinx-serialization-json = "1.7.1"
lifecycle-runtime-compose = "2.8.4" lifecycle-runtime-compose = "2.8.4"
material3 = "1.2.1" material3 = "1.2.1"
multifabVersion = "1.1.0" multifabVersion = "1.1.1"
navigationCompose = "2.7.7" navigationCompose = "2.7.7"
pinLockCompose = "1.0.3" pinLockCompose = "1.0.3"
roomVersion = "2.6.1" roomVersion = "2.6.1"
timber = "5.0.1" timber = "5.0.1"
tunnel = "1.2.1" tunnel = "1.2.3"
androidGradlePlugin = "8.6.0-rc01" androidGradlePlugin = "8.6.0-rc01"
kotlin = "2.0.10" kotlin = "2.0.20"
ksp = "2.0.10-1.0.24" ksp = "2.0.20-1.0.24"
composeBom = "2024.06.00" composeBom = "2024.08.00"
compose = "1.6.8" compose = "1.6.8"
zxingAndroidEmbedded = "4.3.0" zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1" coreSplashscreen = "1.0.1"
@@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow
import java.io.File import java.io.File
interface LocalLogCollector { interface LocalLogCollector {
fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null) suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null)
fun stop() fun stop()
@@ -69,7 +69,7 @@ object LogcatUtil {
internal object Logcat : LocalLogCollector { internal object Logcat : LocalLogCollector {
private var logcatReader: LogcatReader? = null private var logcatReader: LogcatReader? = null
override fun start(onLogMessage: ((message: LogMessage) -> Unit)?) { override suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)?) {
logcatReader ?: run { logcatReader ?: run {
logcatReader = logcatReader =
LogcatReader( LogcatReader(
@@ -78,9 +78,7 @@ object LogcatUtil {
onLogMessage, onLogMessage,
) )
} }
logcatReader?.let { logReader -> logcatReader?.run()
if (!logReader.isAlive) logReader.start()
}
} }
override fun stop() { override fun stop() {
@@ -142,7 +140,7 @@ object LogcatUtil {
pID: String, pID: String,
private val logcatPath: String, private val logcatPath: String,
private val callback: ((input: LogMessage) -> Unit)?, private val callback: ((input: LogMessage) -> Unit)?,
) : Thread() { ) {
private var logcatProc: Process? = null private var logcatProc: Process? = null
private var reader: BufferedReader? = null private var reader: BufferedReader? = null
private var mRunning = true private var mRunning = true
@@ -177,7 +175,7 @@ object LogcatUtil {
}.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") } }.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") }
} }
override fun run() { fun run() {
if (outputStream == null) return if (outputStream == null) return
try { try {
clear() clear()