Compare commits

..

5 Commits

Author SHA1 Message Date
Zane Schepke eeccc71469 fix: tile auto tunnel toggle and wifi internet connection check
Change tile toggle behavior to also toggle auto tunnel service to allow user temporary override and re-enablement of auto tunnel from the tile.

Add tunnel name to tile.

Fix bug where wifi networks without internet access were impacting auto tunneling determinations.

Closes #22
2023-09-01 13:05:17 -04:00
Zane Schepke 0e64bbb4e1 feat: add quick setting tile
Add quick settings tile for easy tunnel toggling and auto-tunnel override.

Fix bug on AndroidTV D-pad tunnel control for multiple tunnels.

Closes #18 , Closes #20
2023-08-30 23:58:03 -04:00
Zane Schepke f513297ba0 fix: file selection on older devices
Fixes bug where file selection was causing app to crash on older devices.
2023-08-11 21:13:54 -04:00
Zane Schepke 135f8c0459 chore: add tv assets 2023-08-11 20:56:03 -04:00
Zane Schepke 7a811f4152 fix: bug causing crashes on older devices
Fixes an issue where watcher service had the potential to crash on older devices if the job was not initialized fast enough.

Optimize imports.

Bump versions.
2023-08-11 20:21:28 -04:00
25 changed files with 265 additions and 54 deletions
+4 -4
View File
@@ -17,7 +17,7 @@ android {
val versionMajor = 2
val versionMinor = 3
val versionPatch = 3
val versionPatch = 7
val versionBuild = 0
defaultConfig {
@@ -89,7 +89,7 @@ dependencies {
implementation("com.jakewharton.timber:timber:5.0.1")
// compose navigation
implementation("androidx.navigation:navigation-compose:2.6.0")
implementation("androidx.navigation:navigation-compose:2.7.1")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
// hilt
@@ -110,7 +110,7 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
//icons
implementation("androidx.compose.material:material-icons-extended:1.4.3")
implementation("androidx.compose.material:material-icons-extended:1.5.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
@@ -122,7 +122,7 @@ dependencies {
implementation("com.google.firebase:firebase-analytics-ktx")
//barcode scanning
implementation("com.google.android.gms:play-services-code-scanner:16.0.0")
implementation("com.google.android.gms:play-services-code-scanner:16.1.0")
}
kapt {
+14
View File
@@ -64,6 +64,20 @@
android:foregroundServiceType="remoteMessaging"
android:exported="false">
</service>
<service
android:exported="true"
android:name=".service.TunnelControlTile"
android:icon="@drawable/shield"
android:label="WG Tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".service.foreground.WireGuardTunnelService"
android:permission="android.permission.BIND_VPN_SERVICE"
@@ -1,7 +1,5 @@
package com.zaneschepke.wireguardautotunnel.module
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.barcode.QRScanner
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
@@ -12,7 +10,6 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent
import dagger.hilt.android.scopes.ServiceScoped
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ServiceComponent::class)
@@ -9,10 +9,9 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -25,7 +24,7 @@ class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
CoroutineScope(SupervisorJob()).launch {
CoroutineScope(Dispatchers.IO).launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
@@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelSer
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -23,7 +23,7 @@ class NotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var settingsRepo : Repository<Settings>
override fun onReceive(context: Context, intent: Intent?) {
CoroutineScope(SupervisorJob()).launch {
CoroutineScope(Dispatchers.IO).launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
@@ -0,0 +1,178 @@
package com.zaneschepke.wireguardautotunnel.service
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TunnelControlTile : TileService() {
@Inject
lateinit var settingsRepo : Repository<Settings>
@Inject
lateinit var configRepo : Repository<TunnelConfig>
@Inject
lateinit var vpnService : VpnService
private val scope = CoroutineScope(Dispatchers.Main);
private lateinit var job : Job
override fun onStartListening() {
job = scope.launch {
updateTileState()
}
super.onStartListening()
}
override fun onTileAdded() {
super.onTileAdded()
qsTile.contentDescription = this.resources.getString(R.string.toggle_vpn)
scope.launch {
updateTileState();
}
}
override fun onTileRemoved() {
super.onTileRemoved()
cancelJob()
}
override fun onClick() {
super.onClick()
unlockAndRun {
scope.launch {
try {
val tunnel = determineTileTunnel();
if(tunnel != null) {
attemptWatcherServiceToggle(tunnel.toString())
if(vpnService.getState() == Tunnel.State.UP) {
stopTunnelService();
} else {
startTunnelService(tunnel.toString())
}
}
} catch (e : Exception) {
Timber.e(e.message)
} finally {
cancel()
}
}
}
}
private suspend fun determineTileTunnel() : TunnelConfig? {
var tunnelConfig : TunnelConfig? = null;
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
tunnelConfig = if (setting.defaultTunnel != null) {
TunnelConfig.from(setting.defaultTunnel!!);
} else {
val config = configRepo.getAll()?.first();
config;
}
}
return tunnelConfig;
}
private fun stopTunnelService() {
ServiceTracker.actionOnService(
Action.STOP, this.applicationContext,
WireGuardTunnelService::class.java)
}
private fun startTunnelService(tunnelConfig : String) {
ServiceTracker.actionOnService(
Action.START, this.applicationContext,
WireGuardTunnelService::class.java,
mapOf(this.applicationContext.resources.
getString(R.string.tunnel_extras_key) to
tunnelConfig))
}
private fun startWatcherService(tunnelConfig : String) {
ServiceTracker.actionOnService(
Action.START, this,
WireGuardConnectivityWatcherService::class.java, mapOf(this.resources.
getString(R.string.tunnel_extras_key) to
tunnelConfig))
}
private fun stopWatcherService() {
ServiceTracker.actionOnService(
Action.STOP, this,
WireGuardConnectivityWatcherService::class.java)
}
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
scope.launch {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if(setting.isAutoTunnelEnabled) {
when(ServiceTracker.getServiceState( this@TunnelControlTile,
WireGuardConnectivityWatcherService::class.java,)) {
ServiceState.STARTED -> stopWatcherService()
ServiceState.STOPPED -> startWatcherService(tunnelConfig)
}
}
}
}
}
private suspend fun updateTileState() {
vpnService.state.collect {
when(it) {
Tunnel.State.UP -> {
qsTile.state = Tile.STATE_ACTIVE
}
Tunnel.State.DOWN -> {
qsTile.state = Tile.STATE_INACTIVE;
}
else -> {
qsTile.state = Tile.STATE_UNAVAILABLE
}
}
val config = determineTileTunnel();
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
qsTile.updateTile()
}
}
private fun setTileDescription(description : String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description;
}
}
private fun cancelJob() {
if(this::job.isInitialized) {
job.cancel();
}
}
}
@@ -4,10 +4,7 @@ import android.app.Service
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import timber.log.Timber
import javax.inject.Inject
open class ForegroundService : Service() {
@@ -24,6 +24,7 @@ object ServiceTracker {
fun <T : Service> actionOnService(action: Action, application: Application, cls : Class<T>, extras : Map<String,String>? = null) {
if (getServiceState(application, cls) == ServiceState.STOPPED && action == Action.STOP) return
if (getServiceState(application, cls) == ServiceState.STARTED && action == Action.START) return
val intent = Intent(application, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
@@ -40,6 +41,7 @@ object ServiceTracker {
fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
val intent = Intent(context, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
@@ -8,8 +8,6 @@ import android.content.Intent
import android.os.Bundle
import android.os.PowerManager
import android.os.SystemClock
import androidx.compose.runtime.collectAsState
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.R
@@ -23,11 +21,9 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -89,7 +85,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
}
cancelWatcherJob()
stopVPN()
stopSelf()
}
@@ -128,20 +123,20 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
private fun startWatcherJob() {
watcherJob = CoroutineScope(SupervisorJob()).launch {
watcherJob = CoroutineScope(Dispatchers.IO).launch {
val settings = settingsRepo.getAll();
if(!settings.isNullOrEmpty()) {
setting = settings[0]
}
CoroutineScope(watcherJob).launch {
launch {
watchForWifiConnectivityChanges()
}
if(setting.isTunnelOnMobileDataEnabled) {
CoroutineScope(watcherJob).launch {
launch {
watchForMobileDataConnectivityChanges()
}
}
CoroutineScope(watcherJob).launch {
launch {
manageVpn()
}
}
@@ -15,7 +15,6 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -42,7 +41,7 @@ class WireGuardTunnelService : ForegroundService() {
super.startService(extras)
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
cancelJob()
job = CoroutineScope(SupervisorJob()).launch {
job = CoroutineScope(Dispatchers.IO).launch {
if(tunnelConfigString != null) {
try {
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
@@ -28,7 +28,6 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
object : ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO
) {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
@@ -68,6 +67,8 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
}
val request = NetworkRequest.Builder()
.addTransportType(networkCapability)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
@@ -30,6 +30,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
override val tunnelName get() = _tunnelName.asStateFlow()
private val _state = MutableSharedFlow<Tunnel.State>(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
replay = 1)
private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1,
@@ -11,5 +11,5 @@ data class Settings(
var isTunnelOnMobileDataEnabled : Boolean = false,
var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
var defaultTunnel : String? = null,
var isAlwaysOnVpnEnabled : Boolean = false
var isAlwaysOnVpnEnabled : Boolean = false,
)
@@ -152,7 +152,7 @@ class MainActivity : AppCompatActivity() {
}
}
}) {
MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController, focusRequester = focusRequester)
MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController)
}
composable(Routes.Settings.name, enterTransition = {
when (initialState.destination.route) {
@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen
@@ -85,7 +86,6 @@ import kotlinx.coroutines.launch
@Composable
fun MainScreen(
viewModel: MainViewModel = hiltViewModel(), padding: PaddingValues,
focusRequester: FocusRequester,
snackbarHostState: SnackbarHostState, navController: NavController
) {
@@ -149,7 +149,7 @@ fun MainScreen(
})
},
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
floatingActionButton = {
AnimatedVisibility(
visible = isVisible.value,
enter = slideInVertically(initialOffsetY = { it * 2 }),
@@ -241,9 +241,12 @@ fun MainScreen(
.padding(padding)
) {
LazyColumn(modifier = Modifier.fillMaxSize()
.nestedScroll(nestedScrollConnection),) {
items(tunnels.toList()) { tunnel ->
LazyColumn(
modifier = Modifier.fillMaxSize()
.nestedScroll(nestedScrollConnection),
) {
itemsIndexed(tunnels.toList()) { index, tunnel ->
val focusRequester = FocusRequester();
RowListItem(leadingIcon = Icons.Rounded.Circle,
leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) {
HandshakeStatus.HEALTHY -> mint
@@ -263,15 +266,15 @@ fun MainScreen(
selectedTunnel = tunnel;
},
onClick = {
if(!WireGuardAutoTunnel.isRunningOnAndroidTv(context)){
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
} else {
focusRequester.requestFocus()
}
},
},
rowButton = {
if (tunnel.id == selectedTunnel?.id) {
Row() {
Row {
IconButton(onClick = {
navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}")
}) {
@@ -287,30 +290,43 @@ fun MainScreen(
}
}
} else {
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)){
Row() {
IconButton(modifier = Modifier.focusRequester(focusRequester),onClick = {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
}) {
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
Row {
IconButton(
modifier = Modifier.focusRequester(focusRequester),
onClick = {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
}) {
Icon(Icons.Rounded.Info, "Info")
}
IconButton(onClick = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
scope.launch {
viewModel.showSnackBarMessage(context.resources.getString(R.string.turn_off_tunnel))
viewModel.showSnackBarMessage(
context.resources.getString(
R.string.turn_off_tunnel
)
)
} else {
navController.navigate("${Routes.Config.name}/${tunnel.id}")
}
navController.navigate("${Routes.Config.name}/${tunnel.id}")
}
}) {
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
Icon(
Icons.Rounded.Edit,
stringResource(id = R.string.edit)
)
}
IconButton(onClick = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
scope.launch {
viewModel.showSnackBarMessage(context.resources.getString(R.string.turn_off_tunnel))
viewModel.showSnackBarMessage(
context.resources.getString(
R.string.turn_off_tunnel
)
)
} else {
viewModel.onDelete(tunnel)
}
viewModel.onDelete(tunnel)
}
}) {
Icon(
Icons.Rounded.Delete,
@@ -144,7 +144,12 @@ class MainViewModel @Inject constructor(private val application : Application,
@SuppressLint("Range")
private fun getFileName(context: Context, uri: Uri): String {
if (uri.scheme == "content") {
val cursor = context.contentResolver.query(uri, null, null, null, null)
val cursor = try {
context.contentResolver.query(uri, null, null, null, null)
} catch (e : Exception) {
Timber.d("Exception getting config name")
null
}
cursor ?: return defaultConfigName()
cursor.use {
if(cursor.moveToFirst()) {
@@ -335,7 +335,7 @@ fun SettingsScreen(
FlowRow(
modifier = Modifier.padding(screenPadding),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
verticalArrangement = Arrangement.SpaceEvenly
) {
trustedSSIDs.forEach { ssid ->
ClickableIconButton(onIconClick = {
+5
View File
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z"/>
</vector>
+2
View File
@@ -84,4 +84,6 @@
<string name="detecting_location_services_disabled">Detecting Location Services disabled</string>
<string name="precise_location_message">This feature requires precise location to access Wi-Fi SSID name. Please enable precise location here or in the app settings.</string>
<string name="request">Request</string>
<string name="toggle_vpn">Toggle VPN</string>
<string name="no_tunnel_available">No tunnels available</string>
</resources>
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

+1 -1
View File
@@ -13,7 +13,7 @@ buildscript {
}
plugins {
id("com.android.application") version "8.2.0-alpha08" apply false
id("com.android.application") version "8.2.0-beta01" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
id("com.google.dagger.hilt.android") version "2.44" apply false
kotlin("plugin.serialization") version "1.8.22" apply false
+1 -1
View File
@@ -1,6 +1,6 @@
#Mon Apr 24 22:46:45 EDT 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists