Compare commits

..

6 Commits

Author SHA1 Message Date
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
33 changed files with 382 additions and 233 deletions
+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
* WireGuard support for kernel and userspace modes
* 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
* Export Amnezia and WireGuard tunnels to zip
* Quick tile support for tunnel toggling, auto-tunneling
* Static shortcuts support for tunnel toggling, auto-tunneling
* Intent automation support for all tunnels
* Automatic auto-tunneling service restart after reboot
* Automatic tunnel restart after reboot
* Automatic auto-tunneling service and/or tunnel restart after reboot or app update
* Battery preservation measures
* Restart tunnel on ping failure (beta)
+12 -1
View File
@@ -58,14 +58,25 @@ android {
)
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) {
initWith(buildTypes.getByName(Constants.RELEASE))
applicationIdSuffix = ".prerelease"
versionNameSuffix = "-pre"
resValue("string", "app_name", "WG Tunnel - Pre")
}
create(Constants.NIGHTLY) {
initWith(buildTypes.getByName(Constants.RELEASE))
applicationIdSuffix = ".nightly"
versionNameSuffix = "-nightly"
resValue("string", "app_name", "WG Tunnel - Nightly")
}
applicationVariants.all {
+17
View File
@@ -167,6 +167,16 @@
android:stopWithTask="false"
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
android:name=".receiver.BootReceiver"
android:enabled="true"
@@ -184,6 +194,13 @@
android:name=".receiver.BackgroundActionReceiver"
android:enabled="true"
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
android:name=".receiver.KernelReceiver"
android:exported="false"
@@ -3,10 +3,15 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import com.zaneschepke.logcatter.LocalLogCollector
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -17,6 +22,13 @@ class WireGuardAutoTunnel : Application() {
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var localLogCollector: LocalLogCollector
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
override fun onCreate() {
super.onCreate()
instance = this
@@ -33,6 +45,11 @@ class WireGuardAutoTunnel : Application() {
} else {
Timber.plant(ReleaseTree())
}
if (!isRunningOnTv()) {
applicationScope.launch(ioDispatcher) {
localLogCollector.start()
}
}
}
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 com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@@ -25,14 +27,19 @@ class BackgroundActionReceiver : BroadcastReceiver() {
@Inject
lateinit var tunnelConfigRepository: TunnelConfigRepository
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getIntExtra(TUNNEL_ID_EXTRA_KEY, 0)
if (id == 0) return
when (intent.action) {
ACTION_CONNECT -> {
Timber.d("Connect actions")
applicationScope.launch {
val tunnel = tunnelConfigRepository.getById(id)
tunnel?.let {
serviceManager.startTunnelBackgroundService(context)
tunnelService.get().startTunnel(it)
}
}
@@ -41,6 +48,7 @@ class BackgroundActionReceiver : BroadcastReceiver() {
applicationScope.launch {
val tunnel = tunnelConfigRepository.getById(id)
tunnel?.let {
serviceManager.stopTunnelBackgroundService(context)
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.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
@@ -35,7 +36,7 @@ class BootReceiver : BroadcastReceiver() {
val settings = appDataRepository.settings.getSettings()
if (settings.isRestoreOnBootEnabled) {
appDataRepository.getStartTunnelConfig()?.let {
tunnelService.get().startTunnel(it)
context.startTunnelBackground(it.id)
}
}
if (settings.isAutoTunnelEnabled) {
@@ -50,4 +50,20 @@ class ServiceManager {
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.AutoTunnelService
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -42,8 +44,8 @@ class ShortcutsActivity : ComponentActivity() {
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelConfig?.let {
when (intent.action) {
Action.START.name -> tunnelService.get().startTunnel(it)
Action.STOP.name -> tunnelService.get().stopTunnel(it)
Action.START.name -> this@ShortcutsActivity.startTunnelBackground(it.id)
Action.STOP.name -> this@ShortcutsActivity.stopTunnelBackground(it.id)
else -> Unit
}
}
@@ -68,6 +68,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
override fun onClick() {
super.onClick()
unlockAndRun {
Timber.d("Click")
lifecycleScope.launch {
val context = this@TunnelControlTile
val lastActive = appDataRepository.getStartTunnelConfig()
@@ -14,4 +14,7 @@ interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun runningTunnelNames(): Set<String>
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.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -56,8 +55,17 @@ constructor(
return runCatching {
when (val backend = backend()) {
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 {
TunnelState.from(it)
is org.amnezia.awg.backend.Backend -> {
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()
}
@@ -144,6 +152,14 @@ constructor(
}
}
override fun cancelStatsJob() {
statsJob?.cancel()
}
override fun startStatsJob() {
statsJob = startTunnelStatisticsJob()
}
override fun getName(): String {
return _vpnState.value.tunnelConfig?.name ?: ""
}
@@ -155,15 +171,9 @@ constructor(
private fun handleStateChange(state: TunnelState) {
emitTunnelState(state)
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
if (state == TunnelState.UP) {
statsJob = startTunnelStatisticsJob()
}
if (state == TunnelState.DOWN) {
try {
statsJob?.cancel()
} catch (e: CancellationException) {
Timber.i("Stats job cancelled")
}
when (state) {
TunnelState.UP -> startStatsJob()
else -> cancelStatsJob()
}
}
@@ -5,6 +5,9 @@ import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
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.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -15,6 +18,7 @@ import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -35,8 +39,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
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.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.StringValue
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
@@ -60,10 +64,7 @@ class MainActivity : AppCompatActivity() {
lateinit var appStateRepository: AppStateRepository
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var serviceManager: ServiceManager
lateinit var tunnelService: TunnelService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -72,13 +73,6 @@ class MainActivity : AppCompatActivity() {
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
lifecycleScope.launch {
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
serviceManager.startWatcherService(application.applicationContext)
}
}
setContent {
val appViewModel = hiltViewModel<AppViewModel>()
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
@@ -127,7 +121,7 @@ class MainActivity : AppCompatActivity() {
)
}
},
// TODO refactor
containerColor = MaterialTheme.colorScheme.background,
modifier =
Modifier
.focusable()
@@ -148,92 +142,97 @@ class MainActivity : AppCompatActivity() {
)
},
) { padding ->
NavHost(
navController,
startDestination = (if (isPinLockEnabled == true) Screen.Lock.route else Screen.Main.route),
modifier =
Modifier
.padding(padding)
.fillMaxSize(),
) {
composable(
Screen.Main.route,
Surface(modifier = Modifier.fillMaxSize().padding(padding)) {
NavHost(
navController,
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
startDestination = (if (isPinLockEnabled == true) Screen.Lock.route else Screen.Main.route),
) {
MainScreen(
focusRequester = focusRequester,
appViewModel = appViewModel,
navController = navController,
)
}
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,
composable(
Screen.Main.route,
) {
MainScreen(
focusRequester = focusRequester,
configType = configType,
appViewModel = appViewModel,
navController = navController,
)
}
}
composable("${Screen.Option.route}/{id}") {
val id = it.arguments?.getString("id")
if (!id.isNullOrBlank()) {
OptionsScreen(
navController = navController,
tunnelId = id,
composable(
Screen.Settings.route,
) {
SettingsScreen(
appViewModel = appViewModel,
navController = navController,
focusRequester = focusRequester,
)
}
}
composable(Screen.Lock.route) {
PinLockScreen(
navController = navController,
appViewModel = appViewModel,
)
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,
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.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.zaneschepke.logcatter.LocalLogCollector
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
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.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
import javax.inject.Provider
@@ -41,11 +36,7 @@ class SplashActivity : ComponentActivity() {
lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var localLogCollector: LocalLogCollector
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
lateinit var serviceManager: ServiceManager
override fun onCreate(savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -54,29 +45,17 @@ class SplashActivity : ComponentActivity() {
}
super.onCreate(savedInstanceState)
applicationScope.launch {
if (!this@SplashActivity.isRunningOnTv()) localLogCollector.start()
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val pinLockEnabled = appStateRepository.isPinLockEnabled()
if (pinLockEnabled) {
PinManager.initialize(WireGuardAutoTunnel.instance)
}
// TODO eventually make this support multi-tunnel
Timber.d("Check for active tunnels")
val settings = appDataRepository.settings.getSettings()
if (settings.isKernelEnabled) {
// delay in case state change is underway while app is opened
delay(Constants.FOCUS_REQUEST_DELAY)
val activeTunnels = appDataRepository.tunnels.getActive()
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)
}
}
if (settings.isAutoTunnelEnabled) serviceManager.startWatcherService(application.applicationContext)
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startStatsJob()
val tunnels = appDataRepository.tunnels.getActive()
if (tunnels.isNotEmpty() && tunnelService.get().getState() == TunnelState.DOWN) tunnelService.get().startTunnel(tunnels.first())
requestTunnelTileServiceStateUpdate()
requestAutoTunnelTileServiceUpdate()
@@ -10,37 +10,29 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
import com.zaneschepke.wireguardautotunnel.ui.Screen
@Composable
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
val backStackEntry = navController.currentBackStackEntryAsState()
var showBottomBar by rememberSaveable { mutableStateOf(true) }
val navBackStackEntry by navController.currentBackStackEntryAsState()
// TODO find a better way to hide nav bar
showBottomBar =
when (navBackStackEntry?.destination?.route) {
Screen.Lock.route -> false
else -> true
}
showBottomBar = bottomNavItems.firstOrNull { navBackStackEntry?.destination?.route?.contains(it.route) == true } != null
NavigationBar(
containerColor = if (!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background,
) {
if (showBottomBar) {
if (showBottomBar) {
NavigationBar(
containerColor = MaterialTheme.colorScheme.surface,
) {
bottomNavItems.forEach { item ->
val selected = item.route == backStackEntry.value?.destination?.route
val selected = navBackStackEntry?.destination?.route?.contains(item.route) == true
NavigationBarItem(
selected = selected,
onClick = {
if (navBackStackEntry?.destination?.route == item.route) return@NavigationBarItem
navController.navigate(item.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
@@ -110,7 +110,12 @@ fun ConfigScreen(
LaunchedEffect(uiState.loading) {
if (!uiState.loading && context.isRunningOnTv()) {
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 amQuick =
if (configType == ConfigType.AMNEZIA) {
buildAmConfig().toAwgQuickString()
buildAmConfig().toAwgQuickString(true)
} else {
TunnelConfig.AM_QUICK_DEFAULT
}
@@ -4,9 +4,6 @@ import android.annotation.SuppressLint
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
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.focusable
import androidx.compose.foundation.gestures.ScrollableDefaults
@@ -147,7 +144,12 @@ fun MainScreen(
LaunchedEffect(Unit) {
if (context.isRunningOnTv()) {
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,
flingBehavior = ScrollableDefaults.flingBehavior(),
) {
item {
AnimatedVisibility(
uiState.tunnels.isEmpty(),
exit = fadeOut(),
enter = fadeIn(),
) {
if (uiState.tunnels.isEmpty()) {
item {
GettingStartedLabel(onClick = { context.openWebUrl(it) })
}
}
@@ -179,7 +179,7 @@ constructor(
when (type) {
ConfigType.AMNEZIA -> {
val config = org.amnezia.awg.config.Config.parse(it)
amQuick = config.toAwgQuickString()
amQuick = config.toAwgQuickString(true)
config.toWgQuickString()
}
@@ -252,7 +252,7 @@ constructor(
org.amnezia.awg.config.Config.parse(
zip,
)
amQuick = config.toAwgQuickString()
amQuick = config.toAwgQuickString(true)
config.toWgQuickString()
}
@@ -88,7 +88,12 @@ fun OptionsScreen(
optionsViewModel.init(tunnelId)
if (context.isRunningOnTv()) {
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.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
@@ -67,7 +66,6 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
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.showToast
import kotlinx.coroutines.launch
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
import java.io.File
@OptIn(
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 {
val pm = context.getSystemService(POWER_SERVICE) as PowerManager
return pm.isIgnoringBatteryOptimizations(context.packageName)
@@ -269,7 +228,7 @@ fun SettingsScreen(
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
checkFineLocationGranted()
}
if(!uiState.isLocationDisclosureShown) {
if (!uiState.isLocationDisclosureShown) {
BackgroundLocationDisclosure(
onDismiss = { viewModel.setLocationDisclosureShown() },
onAttest = {
@@ -282,7 +241,6 @@ fun SettingsScreen(
return
}
BackgroundLocationDialog(
showLocationDialog,
onDismiss = { showLocationDialog = false },
@@ -301,7 +259,13 @@ fun SettingsScreen(
AuthorizationPrompt(
onSuccess = {
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 = { _ ->
showAuthPrompt = false
@@ -380,7 +344,7 @@ fun SettingsScreen(
.focusRequester(focusRequester)
},
)
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
if (uiState.settings.isTunnelOnWifiEnabled) {
Column {
FlowRow(
modifier =
@@ -630,7 +594,7 @@ fun SettingsScreen(
Modifier
.fillMaxWidth(fillMaxWidth)
.padding(vertical = 10.dp)
.padding(bottom = 140.dp),
.padding(bottom = 10.dp),
) {
Column(
horizontalAlignment = Alignment.Start,
@@ -248,4 +248,13 @@ constructor(
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,12 +28,7 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
@Composable
fun BackgroundLocationDisclosure(
onDismiss: () -> Unit,
onAttest: () -> Unit,
scrollState: ScrollState,
focusRequester: FocusRequester,
) {
fun BackgroundLocationDisclosure(onDismiss: () -> Unit, onAttest: () -> Unit, scrollState: ScrollState, focusRequester: FocusRequester) {
val context = LocalContext.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -20,9 +20,10 @@ private val DarkColorScheme =
darkColorScheme(
// primary = Purple80,
primary = virdigris,
secondary = virdigris,
secondary = PurpleGrey40,
// secondary = PurpleGrey80,
tertiary = virdigris,
tertiary = Pink40,
surfaceTint = Pink80,
// tertiary = Pink80
)
@@ -31,6 +32,7 @@ private val LightColorScheme =
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40,
surfaceTint = Pink80,
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
@@ -7,7 +7,7 @@ object Constants {
const val MANUAL_TUNNEL_CONFIG_ID = "0"
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
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 ZIP_FILE_EXTENSION = ".zip"
const val URI_CONTENT_SCHEME = "content"
@@ -24,6 +24,8 @@ object Constants {
const val SUBSCRIPTION_TIMEOUT = 5_000L
const val FOCUS_REQUEST_DELAY = 500L
const val TRANSITION_ANIMATION_TIME = 200
const val DEFAULT_PING_IP = "1.1.1.1"
const val PING_TIMEOUT = 5_000L
const val VPN_RESTART_DELAY = 1_000L
@@ -6,6 +6,8 @@ import android.os.Build
import android.os.Environment
import android.provider.MediaStore
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.withContext
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> {
return withContext(ioDispatcher) {
try {
@@ -29,7 +29,7 @@ fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
}
fun Config.toWgQuickString(): String {
val amQuick = toAwgQuickString()
val amQuick = toAwgQuickString(true)
val lines = amQuick.lines().toMutableList()
val linesIterator = lines.iterator()
while (linesIterator.hasNext()) {
+2 -2
View File
@@ -1,7 +1,7 @@
object Constants {
const val VERSION_NAME = "3.5.0"
const val VERSION_NAME = "3.5.1"
const val JVM_TARGET = "17"
const val VERSION_CODE = 35001
const val VERSION_CODE = 35101
const val TARGET_SDK = 34
const val MIN_SDK = 26
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
+4 -4
View File
@@ -1,7 +1,7 @@
[versions]
accompanist = "0.34.0"
activityCompose = "1.9.1"
amneziawgAndroid = "1.2.1"
amneziawgAndroid = "1.2.2"
androidx-junit = "1.2.1"
appcompat = "1.7.0"
biometricKtx = "1.2.0-alpha05"
@@ -16,16 +16,16 @@ junit = "4.13.2"
kotlinx-serialization-json = "1.7.1"
lifecycle-runtime-compose = "2.8.4"
material3 = "1.2.1"
multifabVersion = "1.1.0"
multifabVersion = "1.1.1"
navigationCompose = "2.7.7"
pinLockCompose = "1.0.3"
roomVersion = "2.6.1"
timber = "5.0.1"
tunnel = "1.2.1"
androidGradlePlugin = "8.7.0-alpha06"
androidGradlePlugin = "8.6.0-rc01"
kotlin = "2.0.10"
ksp = "2.0.10-1.0.24"
composeBom = "2024.06.00"
composeBom = "2024.08.00"
compose = "1.6.8"
zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1"
@@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow
import java.io.File
interface LocalLogCollector {
fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null)
suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null)
fun stop()
@@ -69,7 +69,7 @@ object LogcatUtil {
internal object Logcat : LocalLogCollector {
private var logcatReader: LogcatReader? = null
override fun start(onLogMessage: ((message: LogMessage) -> Unit)?) {
override suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)?) {
logcatReader ?: run {
logcatReader =
LogcatReader(
@@ -78,9 +78,7 @@ object LogcatUtil {
onLogMessage,
)
}
logcatReader?.let { logReader ->
if (!logReader.isAlive) logReader.start()
}
logcatReader?.run()
}
override fun stop() {
@@ -142,7 +140,7 @@ object LogcatUtil {
pID: String,
private val logcatPath: String,
private val callback: ((input: LogMessage) -> Unit)?,
) : Thread() {
) {
private var logcatProc: Process? = null
private var reader: BufferedReader? = null
private var mRunning = true
@@ -177,7 +175,7 @@ object LogcatUtil {
}.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") }
}
override fun run() {
fun run() {
if (outputStream == null) return
try {
clear()