Compare commits

..

6 Commits

Author SHA1 Message Date
Zane Schepke b61da18c41 fmt 2025-08-07 18:01:26 -04:00
Zane Schepke df7b3cf0f5 switch monitor to concurrenthashmap 2025-08-07 17:55:14 -04:00
Zane Schepke b50b37e6c9 remove buffer from message flows 2025-08-07 17:16:34 -04:00
Zane Schepke 1d80da6383 refactor, bugs, edge cases 2025-08-07 07:22:30 -04:00
Zane Schepke fa5a36515f more monitoring change 2025-08-06 21:38:41 -04:00
Zane Schepke 58f53a4267 initial changes 2025-08-06 04:28:51 -04:00
15 changed files with 65 additions and 244 deletions
+1 -6
View File
@@ -118,11 +118,6 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: android_artifacts_${{ inputs.flavor }}
path: >-
app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/${{
inputs.flavor == 'fdroid' && inputs.build_type == 'release'
&& 'wgtunnel-fdroid-release-*.apk'
|| format('wgtunnel-{0}-v*.apk', inputs.flavor)
}}
path: app/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/wgtunnel-${{ inputs.flavor }}${{ inputs.flavor == 'fdroid' && '-release' || '' }}-*.apk
retention-days: 1
if-no-files-found: warn
+2 -5
View File
@@ -121,8 +121,8 @@ android {
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
licensee {
allowedLicenses().forEach { allow(it) }
allowedLicenseUrls().forEach { allowUrl(it) }
Constants.allowedLicenses.forEach { allow(it) }
Constants.allowedLicenseUrls.forEach { allowUrl(it) }
}
applicationVariants.all {
@@ -225,9 +225,6 @@ dependencies {
implementation(libs.shizuku.provider)
implementation(libs.reorderable)
implementation(libs.roomdatabasebackup) {
exclude(group = "org.reactivestreams", module = "reactive-streams")
}
}
tasks.register<Copy>("copyLicenseeJsonToAssets") {
@@ -28,7 +28,6 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
@@ -36,9 +35,6 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
@@ -71,16 +67,11 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import dagger.hilt.android.AndroidEntryPoint
import de.raphaelebner.roomdatabasebackup.core.RoomBackup
import javax.inject.Inject
import kotlin.system.exitProcess
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
import org.amnezia.awg.backend.GoBackend.VpnService
@AndroidEntryPoint
@@ -92,16 +83,8 @@ class MainActivity : AppCompatActivity() {
@Inject lateinit var networkMonitor: NetworkMonitor
@Inject @IoDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject @MainDispatcher lateinit var mainDispatcher: CoroutineDispatcher
@Inject lateinit var appDatabase: AppDatabase
private var lastLocationPermissionState: Boolean? = null
private lateinit var roomBackup: RoomBackup
val REQUEST_CODE = 123
@SuppressLint("BatteryLife")
@@ -114,7 +97,6 @@ class MainActivity : AppCompatActivity() {
window.isNavigationBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
roomBackup = RoomBackup(this)
val viewModel by viewModels<AppViewModel>()
@@ -268,7 +250,7 @@ class MainActivity : AppCompatActivity() {
MainScreen(appUiState, appViewState, viewModel)
}
composable<Route.Settings> {
SettingsScreen(appUiState, appViewState, viewModel)
SettingsScreen(appUiState, viewModel)
}
composable<Route.SettingsAdvanced> {
SettingsAdvancedScreen(appUiState, viewModel)
@@ -356,53 +338,4 @@ class MainActivity : AppCompatActivity() {
super.onPause()
WireGuardAutoTunnel.setUiActive(false)
}
fun performBackup() =
lifecycleScope.launch(ioDispatcher) {
roomBackup
.database(appDatabase)
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
.enableLogDebug(true)
.maxFileCount(5)
.apply {
onCompleteListener { success, message, exitCode ->
lifecycleScope.launch(mainDispatcher) {
if (success) {
showToast(
getString(
R.string.backup_success,
getString(R.string.restarting_app),
)
)
restartApp()
} else showToast(R.string.backup_failed)
}
}
}
.backup()
}
fun performRestore() =
lifecycleScope.launch {
roomBackup
.database(appDatabase)
.enableLogDebug(true)
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
.apply {
onCompleteListener { success, message, exitCode ->
lifecycleScope.launch(mainDispatcher) {
if (success) {
showToast(
getString(
R.string.restore_success,
getString(R.string.restarting_app),
)
)
restartApp()
} else showToast(R.string.restore_failed)
}
}
}
.restore()
}
}
@@ -143,15 +143,6 @@ fun currentNavBackStackEntryAsNavBarState(
NavBarState(
topTitle = { Text(stringResource(R.string.settings)) },
route = Route.Settings,
topTrailing = {
ActionIconButton(Icons.Rounded.Menu, R.string.quick_actions) {
viewModel.handleEvent(
AppEvent.SetBottomSheet(
AppViewState.BottomSheet.BACKUP_AND_RESTORE
)
)
}
},
)
backStackEntry.isCurrentRoute(Route.Appearance::class) ->
@@ -22,24 +22,16 @@ import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.*
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@Composable
fun SettingsScreen(uiState: AppUiState, appViewState: AppViewState, viewModel: AppViewModel) {
fun SettingsScreen(uiState: AppUiState, viewModel: AppViewModel) {
val isTv = LocalIsAndroidTV.current
val focusManager = LocalFocusManager.current
val navController = LocalNavController.current
val interactionSource = remember { MutableInteractionSource() }
when (appViewState.bottomSheet) {
AppViewState.BottomSheet.BACKUP_AND_RESTORE -> {
SettingsBottomSheet(viewModel)
}
else -> Unit
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
@@ -1,78 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.MainActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.state.AppViewState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsBottomSheet(viewModel: AppViewModel) {
val context = LocalContext.current
ModalBottomSheet(
containerColor = MaterialTheme.colorScheme.surface,
onDismissRequest = {
viewModel.handleEvent(AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE))
},
) {
Row(
modifier =
Modifier.fillMaxWidth()
.clickable {
viewModel.handleEvent(
AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE)
)
(context as? MainActivity)?.performBackup()
}
.padding(10.dp)
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.database),
contentDescription = null,
modifier = Modifier.padding(10.dp),
)
Text(
text = stringResource(R.string.backup_application),
modifier = Modifier.padding(10.dp),
)
}
HorizontalDivider()
Row(
modifier =
Modifier.fillMaxWidth()
.clickable {
viewModel.handleEvent(
AppEvent.SetBottomSheet(AppViewState.BottomSheet.NONE)
)
(context as? MainActivity)?.performRestore()
}
.padding(10.dp)
) {
Icon(
imageVector = Icons.Outlined.Restore,
contentDescription = null,
modifier = Modifier.padding(10.dp),
)
Text(
text = stringResource(R.string.restore_application),
modifier = Modifier.padding(10.dp),
)
}
}
}
@@ -22,7 +22,6 @@ data class AppViewState(
}
enum class BottomSheet {
BACKUP_AND_RESTORE,
EXPORT_TUNNELS,
IMPORT_TUNNELS,
LOGS,
@@ -8,6 +8,7 @@ import android.content.Context.POWER_SERVICE
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.location.LocationManager
import android.net.Uri
import android.os.Build
import android.os.PowerManager
@@ -15,16 +16,18 @@ import android.provider.Settings
import android.service.quicksettings.TileService
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.core.location.LocationManagerCompat
import androidx.core.net.toUri
import com.zaneschepke.wireguardautotunnel.MainActivity
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
import com.zaneschepke.wireguardautotunnel.util.Constants
import timber.log.Timber
import java.io.File
import java.io.InputStream
import kotlin.system.exitProcess
import timber.log.Timber
fun Context.openWebUrl(url: String): Result<Unit> {
return kotlin
@@ -110,12 +113,13 @@ fun Context.launchShareFile(file: Uri) {
this.startActivity(chooserIntent)
}
fun Context.showToast(resId: Int) {
Toast.makeText(this, this.getString(resId), Toast.LENGTH_LONG).show()
fun Context.isLocationServicesEnabled(): Boolean {
val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
return LocationManagerCompat.isLocationEnabled(locationManager)
}
fun Context.showToast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
fun Context.showToast(resId: Int) {
Toast.makeText(this, this.getString(resId), Toast.LENGTH_LONG).show()
}
fun Context.launchSupportEmail() {
@@ -235,9 +239,17 @@ fun Activity.setScreenBrightness(brightness: Float) {
window.attributes = window.attributes.apply { screenBrightness = brightness }
}
fun MainActivity.restartApp() {
Intent(this, MainActivity::class.java).also {
startActivity(it)
exitProcess(0)
}
fun Activity.enableImmersiveMode() {
WindowCompat.setDecorFitsSystemWindows(window, false)
val controller = WindowCompat.getInsetsController(window, window.decorView)
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
controller.hide(WindowInsetsCompat.Type.systemBars())
}
fun Activity.disableImmersiveMode() {
WindowCompat.setDecorFitsSystemWindows(window, true)
val controller = WindowCompat.getInsetsController(window, window.decorView)
controller.show(WindowInsetsCompat.Type.systemBars())
window.statusBarColor = android.graphics.Color.TRANSPARENT
}
@@ -35,8 +35,14 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.withFirstState
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import java.io.IOException
import java.net.URL
import java.time.Instant
import java.util.*
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -45,12 +51,6 @@ import org.amnezia.awg.config.Config
import rikka.shizuku.Shizuku
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
import java.io.IOException
import java.net.URL
import java.time.Instant
import java.util.*
import javax.inject.Inject
import javax.inject.Provider
@HiltViewModel
class AppViewModel
@@ -70,7 +70,12 @@ constructor(
private var logsJob: Job? = null
private val _eventChannel = Channel<AppEvent>(Channel.BUFFERED)
private val _eventFlow =
MutableSharedFlow<AppEvent>(
replay = 0,
extraBufferCapacity = 10,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
private val tunnelMutex = Mutex()
private val settingsMutex = Mutex()
@@ -134,7 +139,7 @@ constructor(
if (state.appState.isLocalLogsEnabled) logsJob = startCollectingLogs()
handleTunnelMessages()
}
for (event in _eventChannel) {
_eventFlow.collect { event ->
val state = uiState.value
when (event) {
AppEvent.ToggleLocalLogging ->
@@ -242,7 +247,6 @@ constructor(
is AppEvent.SaveAllConfigs -> saveAllTunnels(event.tunnels)
AppEvent.ToggleShowDetailedPingStats ->
handleToggleShowDetailedPingStats(state.appState)
is AppEvent.SaveMonitoringSettings ->
handleMonitoringSaveChanges(
state.appSettings,
@@ -254,12 +258,10 @@ constructor(
AppEvent.TogglePingMonitoring -> handleTogglePingMonitoring(state.appSettings)
is AppEvent.SetPingAttempts ->
saveSettings(state.appSettings.copy(tunnelPingAttempts = event.count))
is AppEvent.SetPingInterval ->
saveSettings(
state.appSettings.copy(tunnelPingIntervalSeconds = event.interval)
)
is AppEvent.SetPingTimeout ->
saveSettings(
state.appSettings.copy(tunnelPingTimeoutSeconds = event.timeout)
@@ -273,7 +275,7 @@ constructor(
viewModelScope.launch(mainDispatcher) { _uiEvent.emit(event) }
fun handleEvent(event: AppEvent) {
_eventChannel.trySend(event)
_eventFlow.tryEmit(event)
}
private suspend fun handleTogglePingMonitoring(appSettings: AppSettings) {
-9
View File
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,840q-151,0 -255.5,-46.5T120,680v-400q0,-66 105.5,-113T480,120q149,0 254.5,47T840,280v400q0,67 -104.5,113.5T480,840ZM480,361q89,0 179,-25.5T760,281q-11,-29 -100.5,-55T480,200q-91,0 -178.5,25.5T200,281q14,30 101.5,55T480,361ZM480,560q42,0 81,-4t74.5,-11.5q35.5,-7.5 67,-18.5t57.5,-25v-120q-26,14 -57.5,25t-67,18.5Q600,432 561,436t-81,4q-42,0 -82,-4t-75.5,-11.5Q287,417 256,406t-56,-25v120q25,14 56,25t66.5,18.5Q358,552 398,556t82,4ZM480,760q46,0 93.5,-7t87.5,-18.5q40,-11.5 67,-26t32,-29.5v-98q-26,14 -57.5,25t-67,18.5Q600,632 561,636t-81,4q-42,0 -82,-4t-75.5,-11.5Q287,617 256,606t-56,-25v99q5,15 31.5,29t66.5,25.5q40,11.5 88,18.5t94,7Z"
android:fillColor="#e3e3e3"/>
</vector>
-7
View File
@@ -327,11 +327,4 @@
<string name="ping_target_template">Ping target: %1$s</string>
<string name="_true">True</string>
<string name="_false">False</string>
<string name="backup_success">Backup success. %1$s</string>
<string name="restore_success">Restore success. %1$s</string>
<string name="restarting_app">Restarting app to apply changes…</string>
<string name="restore_failed">Failed to restore from backup.</string>
<string name="backup_failed">Failed to create backup.</string>
<string name="backup_application">Backup application data</string>
<string name="restore_application">Restore from backup</string>
</resources>
+5
View File
@@ -1,5 +1,6 @@
object Constants {
const val VERSION_NAME = "3.9.4"
const val JVM_TARGET = "17"
const val VERSION_CODE = 39400
const val TARGET_SDK = 35
const val MIN_SDK = 26
@@ -9,4 +10,8 @@ object Constants {
// build types
const val RELEASE = "release"
const val NIGHTLY = "nightly"
val allowedLicenses = listOf("MIT", "Apache-2.0", "BSD-3-Clause")
val allowedLicenseUrls = listOf("https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING",
"https://github.com/RikkaApps/Shizuku-API/blob/master/LICENSE")
}
-9
View File
@@ -15,15 +15,6 @@ fun Project.languageList(): List<String> {
.toList() + "en"
}
fun allowedLicenses(): List<String> {
return listOf("MIT", "Apache-2.0", "BSD-3-Clause")
}
fun allowedLicenseUrls(): List<String> {
return listOf("https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING",
"https://github.com/RikkaApps/Shizuku-API/blob/master/LICENSE", "https://github.com/rafi0101/Android-Room-Database-Backup/blob/master/LICENSE",
"https://opensource.org/license/mit")
}
fun buildLanguagesArray(languages: List<String>): String {
return languages.joinToString(separator = ", ") { "\"$it\"" }
+14 -16
View File
@@ -1,39 +1,38 @@
[versions]
accompanist = "0.37.3"
activityCompose = "1.10.1"
amneziawgAndroid = "1.4.0"
androidx-junit = "1.3.0"
amneziawgAndroid = "1.5.0"
androidx-junit = "1.2.1"
icmp4a = "1.0.0"
roomdatabasebackup = "1.1.0"
shizuku = "13.1.5"
appcompat = "1.7.1"
biometricKtx = "1.2.0-alpha05"
coreKtx = "1.16.0"
datastorePreferences = "1.2.0-alpha02"
desugar_jdk_libs = "2.1.5"
espressoCore = "3.7.0"
hiltAndroid = "2.57"
espressoCore = "3.6.1"
hiltAndroid = "2.56.2"
hiltCompiler = "1.2.0"
junit = "4.13.2"
kotlinx-serialization-json = "1.9.0"
ktorClientCore = "3.2.3"
kotlinx-serialization-json = "1.8.1"
ktorClientCore = "3.1.3"
lifecycle-runtime-compose = "2.9.2"
material3 = "1.3.2"
navigationCompose = "2.9.3"
navigationCompose = "2.9.0"
pinLockCompose = "1.0.4"
qrose = "1.0.1"
roomVersion = "2.7.2"
roomVersion = "2.7.1"
semver4j = "3.1.0"
slf4jAndroid = "1.7.36"
timber = "5.0.1"
tunnel = "1.4.0"
androidGradlePlugin = "8.11.0"
androidGradlePlugin = "8.10.1"
kotlin = "2.2.0"
ksp = "2.2.0-2.0.2"
composeBom = "2025.07.00"
compose = "1.8.3"
composeBom = "2025.06.00"
compose = "1.8.2"
icons = "1.7.8"
workRuntimeKtxVersion = "2.10.3"
workRuntimeKtxVersion = "2.10.1"
zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1"
gradlePlugins-grgit = "5.3.2"
@@ -41,8 +40,8 @@ reorderable = "2.5.1"
#plugins
material = "1.12.0"
storage = "1.6.0"
ktfmt = "0.23.0"
storage = "1.5.0"
ktfmt = "0.22.0"
licensee = "1.13.0"
@@ -105,7 +104,6 @@ material-icons-extended = { module = "androidx.compose.material:material-icons-e
# util
pin-lock-compose = { module = "com.zaneschepke:pin_lock_compose", version.ref = "pinLockCompose" }
roomdatabasebackup = { module = "de.raphaelebner:roomdatabasebackup", version.ref = "roomdatabasebackup" }
shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" }
shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" }
qrose = { module = "io.github.alexzhirkevich:qrose", version.ref = "qrose" }
+2 -2
View File
@@ -1,8 +1,8 @@
#Wed Oct 11 22:39:21 EDT 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionSha256Sum=f397b287023acdba1e9f6fc5ea72d22dd63669d59ed4a289a29b1a76eee151c6
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists