Compare commits

..

17 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
Zane Schepke 2abf681d17 feat: support Android 9
Added support for Android 9 by updating permission checks and wifi SSID logic.

Fix bug where setting screen was cut off on AndroidTV by updating padding values.

Bump wireguard-android library version.

Closes #13, Closes #16
2023-08-04 16:43:36 -04:00
Zane Schepke 689c97f452 fix: auto tunneling failure with rapid network changes
Fixes issue where rapid network switching could cause unexpected VPN connections and disconnection. Fixed by changing from real time network VPN triggers to three second interval checks.

Attempts to optimize battery drain while VPN connected by switching VPN statistics gathering to 10 second intervals.

Closes #11, Closes #12, Closes #10
2023-08-01 17:39:11 -04:00
Zane Schepke 08d11a53b4 fix: AndroidTV D-pad control and banner
Fix Android TV D-pad access to elements on screen without reloading screen

Update AndroidTV banner
2023-07-31 23:38:51 -04:00
Zane Schepke 9952e97e1c Merge branch 'main' of github.com:zaneschepke/wgtunnel 2023-07-29 18:18:26 -04:00
Zane Schepke 4cdc974778 fix: AndroidTV D-pad support and precise location check
Allows app be fully navigated via D-pad on AndroidTV (with some quirks)

Adds precise location check to setting screen as WiFi-SSID is not readable without this permission.

Closes #2
2023-07-29 18:18:24 -04:00
Zane Schepke e31a4c03cd docs: update README.md 2023-07-27 17:21:01 -04:00
Zane Schepke 5b94f22359 docs: update README.md 2023-07-27 17:19:25 -04:00
Zane Schepke c673a8dc91 feat: support for Always-On VPN and Android TV
Added support for Android TV

Added support for Always-On VPN in settings

Fixes bug where handshake notification is not dismissed after successful handshake

Fixes bug that allowed you to use Auto tunneling without Location Services enabled. Now checks for Location Services before allowing access to feature.

Closes #2,  Closes #5, Closes #8
2023-07-23 00:51:19 -04:00
Zane Schepke f6612abe28 fix: service requirements for Android 14
Fixes requirement for foregroundServiceType which is causing crashes on Android 14
2023-07-20 14:23:43 -04:00
Zane Schepke 7ca5de1836 fix: screens not scrollable in landscape
Fixes not being able to scroll when in landscape mode if content is off screen.

Fixes FAB being in the way of controlling tunnels by making it disappear on scroll down.

Bump targetSdk to 34
2023-07-19 09:02:31 -04:00
Zane Schepke 509d22a98c build: disable crashlytics on debug builds 2023-07-18 17:07:36 -04:00
Zane Schepke 68b0902398 feat: add tunnel details screen and handshake monitoring
Adds details screen which display details of tunnel configuration as well as last handshake and rx/tx of peer.

Adds last handshake monitoring with statuses and thresholds.

Adds handshake/connection notifications based on last successful handshake.

Adds status LED next to tunnel on main screen.

Fixes bug where first click on QR code could result in nothing happening if QR code module is being downloaded. Now shows message to user.

Fixes bug where changes made after editing tunnel were not propagated to settings if that tunnel was configured as the default tunnel.

Fixes bug causing crash if wrong config file selected

Update README

Closes #7, Closes #6
2023-07-18 11:53:03 -04:00
46 changed files with 847 additions and 275 deletions
+10 -2
View File
@@ -16,6 +16,13 @@ WG Tunnel
</span> </span>
<span align="center">
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/N4N8NMJN2)
</span>
<span align="left"> <span align="left">
This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android) library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app. This is an alternative Android Application for [WireGuard](https://www.wireguard.com/) with added features. Built using the [wireguard-android](https://github.com/WireGuard/wireguard-android) library and [Jetpack Compose](https://developer.android.com/jetpack/compose), this application was inspired by the official [WireGuard Android](https://github.com/WireGuard/wireguard-android) app.
@@ -29,8 +36,8 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard
<p float="center"> <p float="center">
<img label="Main" style="padding-right:25px" src="asset/main_screen.png" width="200" /> <img label="Main" style="padding-right:25px" src="asset/main_screen.png" width="200" />
<img label="Config" style="padding-left:25px" src="./asset/config_screen.png" width="200" /> <img label="Config" style="padding-left:25px" src="./asset/config_screen.png" width="200" />
<img label="Settings" style="padding-left:25px" src="./asset/settings_screen.png" width="200" /> <img label="Settings" style="padding-left:25px" src="asset/settings_screen.png" width="200" />
<img label="Support" style="padding-left:25px" src="./asset/support_screen.png" width="200" /> <img label="Support" style="padding-left:25px" src="asset/support_screen.png" width="200" />
</p> </p>
<span align="left"> <span align="left">
@@ -44,6 +51,7 @@ The inspiration for this app came from the inconvenience of constantly having to
* Add tunnels via .conf file * Add tunnels via .conf file
* Auto connect to VPN based on Wi-Fi SSID * Auto connect to VPN based on Wi-Fi SSID
* Split tunneling by application * Split tunneling by application
* Always-on VPN for Android support
* Configurable Trusted Network list * Configurable Trusted Network list
* Optional auto connect on mobile data * Optional auto connect on mobile data
* Automatic service restart after reboot * Automatic service restart after reboot
+10 -13
View File
@@ -5,25 +5,25 @@ plugins {
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
kotlin("kapt") kotlin("kapt")
id("com.google.dagger.hilt.android") id("com.google.dagger.hilt.android")
id("io.objectbox")
id("com.google.gms.google-services") id("com.google.gms.google-services")
id("com.google.firebase.crashlytics") id("com.google.firebase.crashlytics")
id("org.jetbrains.kotlin.plugin.serialization") id("org.jetbrains.kotlin.plugin.serialization")
id("io.objectbox")
} }
android { android {
namespace = "com.zaneschepke.wireguardautotunnel" namespace = "com.zaneschepke.wireguardautotunnel"
compileSdk = 33 compileSdk = 34
val versionMajor = 2 val versionMajor = 2
val versionMinor = 1 val versionMinor = 3
val versionPatch = 1 val versionPatch = 7
val versionBuild = 0 val versionBuild = 0
defaultConfig { defaultConfig {
applicationId = "com.zaneschepke.wireguardautotunnel" applicationId = "com.zaneschepke.wireguardautotunnel"
minSdk = 29 minSdk = 28
targetSdk = 33 targetSdk = 34
versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
versionName = "${versionMajor}.${versionMinor}.${versionPatch}" versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
@@ -83,13 +83,13 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-test-manifest") debugImplementation("androidx.compose.ui:ui-test-manifest")
//wireguard tunnel //wireguard tunnel
implementation("com.wireguard.android:tunnel:1.0.20230427") implementation("com.wireguard.android:tunnel:1.0.20230706")
//logging //logging
implementation("com.jakewharton.timber:timber:5.0.1") implementation("com.jakewharton.timber:timber:5.0.1")
// compose navigation // 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") implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
// hilt // hilt
@@ -110,7 +110,7 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1") implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
//icons //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") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
@@ -122,10 +122,7 @@ dependencies {
implementation("com.google.firebase:firebase-analytics-ktx") implementation("com.google.firebase:firebase-analytics-ktx")
//barcode scanning //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 { kapt {
+6 -1
View File
@@ -31,7 +31,7 @@
}, },
{ {
"id": "2:8887605597748372702", "id": "2:8887605597748372702",
"lastPropertyId": "8:4981008812459251156", "lastPropertyId": "9:4468844863383145378",
"name": "Settings", "name": "Settings",
"properties": [ "properties": [
{ {
@@ -59,6 +59,11 @@
"id": "6:3370284381040192129", "id": "6:3370284381040192129",
"name": "defaultTunnel", "name": "defaultTunnel",
"type": 9 "type": 9
},
{
"id": "9:4468844863383145378",
"name": "isAlwaysOnVpnEnabled",
"type": 1
} }
], ],
"relations": [] "relations": []
+2 -6
View File
@@ -59,11 +59,6 @@
"id": "6:3370284381040192129", "id": "6:3370284381040192129",
"name": "defaultTunnel", "name": "defaultTunnel",
"type": 9 "type": 9
},
{
"id": "8:4981008812459251156",
"name": "showProminentDisclosure",
"type": 1
} }
], ],
"relations": [] "relations": []
@@ -91,7 +86,8 @@
7555225587864607050, 7555225587864607050,
969146862000617878, 969146862000617878,
5057486545428188436, 5057486545428188436,
2814640993034665120 2814640993034665120,
4981008812459251156
], ],
"retiredRelationUids": [], "retiredRelationUids": [],
"version": 1 "version": 1
+44 -3
View File
@@ -1,23 +1,36 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
android:maxSdkVersion="30" /> android:maxSdkVersion="30"
tools:ignore="LeanbackUsesWifi" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>
<!--foreground service permissions--> <!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<!--start service on boot permission--> <!--start service on boot permission-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!--android tv support-->
<uses-feature android:name="android.software.leanback"
android:required="false" />
<uses-feature android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.portrait"
android:required="false" />
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -29,6 +42,7 @@
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:banner="@mipmap/ic_banner"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
@@ -40,24 +54,48 @@
android:theme="@style/Theme.WireguardAutoTunnel"> android:theme="@style/Theme.WireguardAutoTunnel">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<service <service
android:name=".service.foreground.ForegroundService" android:name=".service.foreground.ForegroundService"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="remoteMessaging"
android:exported="false"> android:exported="false">
</service> </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 <service
android:name=".service.foreground.WireGuardTunnelService" android:name=".service.foreground.WireGuardTunnelService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="remoteMessaging"
android:exported="false"> android:exported="false">
<intent-filter>
<action android:name="android.net.VpnService"/>
</intent-filter>
<meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="true"/>
</service> </service>
<service <service
android:name=".service.foreground.WireGuardConnectivityWatcherService" android:name=".service.foreground.WireGuardConnectivityWatcherService"
android:enabled="true" android:enabled="true"
android:stopWithTask="false" android:stopWithTask="false"
android:foregroundServiceType="location"
android:permission=""
android:exported="false"> android:exported="false">
</service> </service>
<receiver android:enabled="true" android:name=".receiver.BootReceiver" <receiver android:enabled="true" android:name=".receiver.BootReceiver"
@@ -70,5 +108,8 @@
<meta-data <meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES" android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui"/> android:value="barcode_ui"/>
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="true" />
</application> </application>
</manifest> </manifest>
@@ -0,0 +1,6 @@
package com.zaneschepke.wireguardautotunnel
object Constants {
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L;
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L;
}
@@ -1,6 +1,9 @@
package com.zaneschepke.wireguardautotunnel package com.zaneschepke.wireguardautotunnel
import android.app.Application import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.zaneschepke.wireguardautotunnel.repository.Repository import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
@@ -15,7 +18,16 @@ class WireGuardAutoTunnel : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if(BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) if(BuildConfig.DEBUG) {
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false);
Timber.plant(Timber.DebugTree())
}
settingsRepo.init() settingsRepo.init()
} }
companion object {
fun isRunningOnAndroidTv(context : Context) : Boolean {
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
}
}
} }
@@ -7,7 +7,6 @@ import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.barcode.QRScanner import com.zaneschepke.wireguardautotunnel.service.barcode.QRScanner
import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@@ -1,7 +1,5 @@
package com.zaneschepke.wireguardautotunnel.module 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.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.WifiService import com.zaneschepke.wireguardautotunnel.service.network.WifiService
@@ -12,7 +10,6 @@ import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent import dagger.hilt.android.components.ServiceComponent
import dagger.hilt.android.scopes.ServiceScoped import dagger.hilt.android.scopes.ServiceScoped
import dagger.hilt.android.scopes.ViewModelScoped
@Module @Module
@InstallIn(ServiceComponent::class) @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.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@@ -25,7 +24,7 @@ class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) { if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
CoroutineScope(SupervisorJob()).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
val settings = settingsRepo.getAll() val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) { if (!settings.isNullOrEmpty()) {
@@ -11,7 +11,7 @@ import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelSer
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -23,7 +23,7 @@ class NotificationActionReceiver : BroadcastReceiver() {
@Inject @Inject
lateinit var settingsRepo : Repository<Settings> lateinit var settingsRepo : Repository<Settings>
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) {
CoroutineScope(SupervisorJob()).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
val settings = settingsRepo.getAll() val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) { 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();
}
}
}
@@ -24,6 +24,10 @@ open class ForegroundService : Service() {
when (action) { when (action) {
Action.START.name -> startService(intent.extras) Action.START.name -> startService(intent.extras)
Action.STOP.name -> stopService(intent.extras) Action.STOP.name -> stopService(intent.extras)
"android.net.VpnService" -> {
Timber.d("Always-on VPN starting service")
startService(intent.extras)
}
else -> Timber.d("This should never happen. No action in the received intent") else -> Timber.d("This should never happen. No action in the received intent")
} }
} else { } else {
@@ -24,6 +24,7 @@ object ServiceTracker {
fun <T : Service> actionOnService(action: Action, application: Application, cls : Class<T>, extras : Map<String,String>? = null) { 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.STOPPED && action == Action.STOP) return
if (getServiceState(application, cls) == ServiceState.STARTED && action == Action.START) return
val intent = Intent(application, cls).also { val intent = Intent(application, cls).also {
it.action = action.name it.action = action.name
extras?.forEach {(k, v) -> 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) { 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.STOPPED && action == Action.STOP) return
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
val intent = Intent(context, cls).also { val intent = Intent(context, cls).also {
it.action = action.name it.action = action.name
extras?.forEach {(k, v) -> extras?.forEach {(k, v) ->
@@ -9,6 +9,7 @@ import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import android.os.SystemClock import android.os.SystemClock
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
@@ -20,8 +21,9 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -46,15 +48,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
@Inject @Inject
lateinit var vpnService : VpnService lateinit var vpnService : VpnService
private var isWifiConnected = false;
private var isMobileDataConnected = false;
private var currentNetworkSSID = "";
private lateinit var watcherJob : Job; private lateinit var watcherJob : Job;
private lateinit var setting : Settings private lateinit var setting : Settings
private lateinit var tunnelId: String private lateinit var tunnelId: String
private var connecting = false
private var disconnecting = false
private var isWifiConnected = false
private var isMobileDataConnected = false
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private val tag = this.javaClass.name; private val tag = this.javaClass.name;
@@ -84,7 +85,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
} }
cancelWatcherJob() cancelWatcherJob()
stopVPN()
stopSelf() stopSelf()
} }
@@ -123,19 +123,22 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
private fun startWatcherJob() { private fun startWatcherJob() {
watcherJob = CoroutineScope(SupervisorJob()).launch { watcherJob = CoroutineScope(Dispatchers.IO).launch {
val settings = settingsRepo.getAll(); val settings = settingsRepo.getAll();
if(!settings.isNullOrEmpty()) { if(!settings.isNullOrEmpty()) {
setting = settings[0] setting = settings[0]
} }
CoroutineScope(watcherJob).launch { launch {
watchForWifiConnectivityChanges() watchForWifiConnectivityChanges()
} }
if(setting.isTunnelOnMobileDataEnabled) { if(setting.isTunnelOnMobileDataEnabled) {
CoroutineScope(watcherJob).launch { launch {
watchForMobileDataConnectivityChanges() watchForMobileDataConnectivityChanges()
} }
} }
launch {
manageVpn()
}
} }
} }
@@ -149,17 +152,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
isMobileDataConnected = true isMobileDataConnected = true
Timber.d("Mobile data capabilities changed") Timber.d("Mobile data capabilities changed")
if(!disconnecting && !connecting) {
if(!isWifiConnected && setting.isTunnelOnMobileDataEnabled
&& vpnService.getState() == Tunnel.State.DOWN)
startVPN()
}
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
isMobileDataConnected = false isMobileDataConnected = false
if(!disconnecting && !connecting) {
if(!isWifiConnected && vpnService.getState() == Tunnel.State.UP) stopVPN()
}
Timber.d("Lost mobile data connection") Timber.d("Lost mobile data connection")
} }
} }
@@ -176,61 +171,52 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Wifi capabilities changed") Timber.d("Wifi capabilities changed")
isWifiConnected = true isWifiConnected = true
if (!connecting && !disconnecting) { currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: "";
Timber.d("Not connect and not disconnecting")
val ssid = wifiService.getNetworkName(it.networkCapabilities);
Timber.d("SSID: $ssid")
if (!setting.trustedNetworkSSIDs.contains(ssid) && vpnService.getState() == Tunnel.State.DOWN) {
Timber.d("Starting VPN Tunnel for untrusted network: $ssid")
startVPN()
} else if (!disconnecting && vpnService.getState() == Tunnel.State.UP && setting.trustedNetworkSSIDs.contains(
ssid
)
) {
Timber.d("Stopping VPN Tunnel for trusted network with ssid: $ssid")
stopVPN()
}
}
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
isWifiConnected = false isWifiConnected = false
Timber.d("Lost Wi-Fi connection") Timber.d("Lost Wi-Fi connection")
if(!connecting || !disconnecting) {
if(setting.isTunnelOnMobileDataEnabled && vpnService.getState() == Tunnel.State.DOWN
&& isMobileDataConnected){
Timber.d("Wifi not available so starting vpn for mobile data")
startVPN()
}
if(!setting.isTunnelOnMobileDataEnabled && vpnService.getState() == Tunnel.State.UP) {
Timber.d("Lost WiFi connection, disabling vpn")
stopVPN()
}
}
} }
} }
} }
} }
private fun startVPN() {
if(!connecting) { private suspend fun manageVpn() {
connecting = true while(watcherJob.isActive) {
ServiceTracker.actionOnService( if(setting.isTunnelOnMobileDataEnabled &&
Action.START, !isWifiConnected &&
this.applicationContext as Application, isMobileDataConnected
WireGuardTunnelService::class.java, && vpnService.getState() == Tunnel.State.DOWN) {
mapOf(getString(R.string.tunnel_extras_key) to tunnelId)) startVPN()
connecting = false } else if(!setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
vpnService.getState() == Tunnel.State.UP) {
stopVPN()
} else if(isWifiConnected &&
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
(vpnService.getState() != Tunnel.State.UP)) {
startVPN()
} else if((isWifiConnected &&
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(vpnService.getState() == Tunnel.State.UP)) {
stopVPN()
}
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
} }
} }
private fun startVPN() {
ServiceTracker.actionOnService(
Action.START,
this.applicationContext as Application,
WireGuardTunnelService::class.java,
mapOf(getString(R.string.tunnel_extras_key) to tunnelId))
}
private fun stopVPN() { private fun stopVPN() {
if(!disconnecting) { ServiceTracker.actionOnService(
disconnecting = true Action.STOP,
ServiceTracker.actionOnService( this.applicationContext as Application,
Action.STOP, WireGuardTunnelService::class.java
this.applicationContext as Application, )
WireGuardTunnelService::class.java
)
disconnecting = false
}
} }
} }
@@ -5,15 +5,16 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -26,6 +27,9 @@ class WireGuardTunnelService : ForegroundService() {
@Inject @Inject
lateinit var vpnService : VpnService lateinit var vpnService : VpnService
@Inject
lateinit var settingsRepo: Repository<Settings>
@Inject @Inject
lateinit var notificationService : NotificationService lateinit var notificationService : NotificationService
@@ -37,7 +41,7 @@ class WireGuardTunnelService : ForegroundService() {
super.startService(extras) super.startService(extras)
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key)) val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
cancelJob() cancelJob()
job = CoroutineScope(SupervisorJob()).launch { job = CoroutineScope(Dispatchers.IO).launch {
if(tunnelConfigString != null) { if(tunnelConfigString != null) {
try { try {
val tunnelConfig = TunnelConfig.from(tunnelConfigString) val tunnelConfig = TunnelConfig.from(tunnelConfigString)
@@ -48,7 +52,16 @@ class WireGuardTunnelService : ForegroundService() {
stopService(extras) stopService(extras)
} }
} else { } else {
Timber.e("Tunnel config null") Timber.d("Tunnel config null, starting default tunnel")
val settings = settingsRepo.getAll();
if(!settings.isNullOrEmpty()) {
val setting = settings[0]
if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
tunnelName = tunnelConfig.name
vpnService.startTunnel(tunnelConfig)
}
}
} }
} }
CoroutineScope(job).launch { CoroutineScope(job).launch {
@@ -62,6 +75,7 @@ class WireGuardTunnelService : ForegroundService() {
if(!didShowFailedHandshakeNotification) { if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message)) launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message))
didShowFailedHandshakeNotification = true didShowFailedHandshakeNotification = true
didShowConnected = false
} }
} }
HandshakeStatus.HEALTHY -> { HandshakeStatus.HEALTHY -> {
@@ -74,6 +88,7 @@ class WireGuardTunnelService : ForegroundService() {
if(!didShowFailedHandshakeNotification) { if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message)) launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message))
didShowFailedHandshakeNotification = true didShowFailedHandshakeNotification = true
didShowConnected = false
} }
} }
} }
@@ -28,7 +28,6 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
object : ConnectivityManager.NetworkCallback( object : ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO FLAG_INCLUDE_LOCATION_INFO
) { ) {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network)) trySend(NetworkStatus.Available(network))
} }
@@ -45,6 +44,7 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
} }
} }
} }
else -> { else -> {
object : ConnectivityManager.NetworkCallback() { object : ConnectivityManager.NetworkCallback() {
@@ -67,6 +67,8 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
} }
val request = NetworkRequest.Builder() val request = NetworkRequest.Builder()
.addTransportType(networkCapability) .addTransportType(networkCapability)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build() .build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback) connectivityManager.registerNetworkCallback(request, networkStatusCallback)
@@ -77,8 +79,8 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? { override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
var ssid : String? = getWifiNameFromCapabilities(networkCapabilities) var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
if((Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R)) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
val info = wifiManager.connectionInfo val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) { if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid ssid = info.ssid
@@ -89,14 +91,15 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
companion object { companion object {
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities) : String? { private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? {
val info : WifiInfo if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if(networkCapabilities.transportInfo is WifiInfo) { val info: WifiInfo
info = networkCapabilities.transportInfo as WifiInfo if (networkCapabilities.transportInfo is WifiInfo) {
} else { info = networkCapabilities.transportInfo as WifiInfo
return null return info.ssid
}
} }
return info.ssid return null
} }
} }
} }
@@ -5,6 +5,7 @@ import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Statistics import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.wireguard.crypto.Key import com.wireguard.crypto.Key
import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -29,6 +30,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
override val tunnelName get() = _tunnelName.asStateFlow() override val tunnelName get() = _tunnelName.asStateFlow()
private val _state = MutableSharedFlow<Tunnel.State>( private val _state = MutableSharedFlow<Tunnel.State>(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
replay = 1) replay = 1)
private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1, private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1,
@@ -103,7 +105,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
_handshakeStatus.emit(HandshakeStatus.NOT_STARTED) _handshakeStatus.emit(HandshakeStatus.NOT_STARTED)
} }
if(neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) { if(neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
neverHadHandshakeCounter++ neverHadHandshakeCounter += 10
} }
return@forEach return@forEach
} }
@@ -114,7 +116,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
} }
} }
_lastHandshake.emit(handshakeMap) _lastHandshake.emit(handshakeMap)
delay(1000) delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
} }
} }
} }
@@ -10,5 +10,6 @@ data class Settings(
var isAutoTunnelEnabled : Boolean = false, var isAutoTunnelEnabled : Boolean = false,
var isTunnelOnMobileDataEnabled : Boolean = false, var isTunnelOnMobileDataEnabled : Boolean = false,
var trustedNetworkSSIDs : MutableList<String> = mutableListOf(), var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
var defaultTunnel : String? = null var defaultTunnel : String? = null,
var isAlwaysOnVpnEnabled : Boolean = false,
) )
@@ -6,6 +6,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.view.KeyEvent
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -22,6 +23,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.key.onKeyEvent
import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.navigation.animation.rememberAnimatedNavController
@@ -40,6 +44,8 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import java.lang.IllegalStateException
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@@ -51,6 +57,8 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
val navController = rememberAnimatedNavController() val navController = rememberAnimatedNavController()
val focusRequester = remember { FocusRequester() }
WireguardAutoTunnelTheme { WireguardAutoTunnelTheme {
TransparentSystemBars() TransparentSystemBars()
@@ -80,7 +88,25 @@ class MainActivity : AppCompatActivity() {
} else requestNotificationPermission() } else requestNotificationPermission()
} }
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }, Scaffold(snackbarHost = { SnackbarHost(snackbarHostState)},
modifier = Modifier.onKeyEvent {
if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {
when (it.nativeKeyEvent.keyCode) {
KeyEvent.KEYCODE_DPAD_UP -> {
try {
focusRequester.requestFocus()
} catch(e : IllegalStateException) {
Timber.e("No D-Pad focus request modifier added to element on screen")
}
false
} else -> {
false;
}
}
} else {
false
}
},
bottomBar = if (vpnIntent == null && notificationPermissionState.status.isGranted) { bottomBar = if (vpnIntent == null && notificationPermissionState.status.isGranted) {
{ BottomNavBar(navController, Routes.navItems) } { BottomNavBar(navController, Routes.navItems) }
} else { } else {
@@ -147,7 +173,7 @@ class MainActivity : AppCompatActivity() {
fadeIn(animationSpec = tween(1000)) fadeIn(animationSpec = tween(1000))
} }
} }
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController) } }) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController, focusRequester = focusRequester) }
composable(Routes.Support.name, enterTransition = { composable(Routes.Support.name, enterTransition = {
when (initialState.destination.route) { when (initialState.destination.route) {
Routes.Settings.name, Routes.Main.name -> Routes.Settings.name, Routes.Main.name ->
@@ -160,10 +186,10 @@ class MainActivity : AppCompatActivity() {
fadeIn(animationSpec = tween(1000)) fadeIn(animationSpec = tween(1000))
} }
} }
}) { SupportScreen(padding = padding) } }) { SupportScreen(padding = padding, focusRequester) }
composable("${Routes.Config.name}/{id}", enterTransition = { composable("${Routes.Config.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(1000)) fadeIn(animationSpec = tween(1000))
}) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"))} }) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"), focusRequester = focusRequester)}
composable("${Routes.Detail.name}/{id}", enterTransition = { composable("${Routes.Detail.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(1000)) fadeIn(animationSpec = tween(1000))
}) { DetailScreen(padding = padding, id = it.arguments?.getString("id")) } }) { DetailScreen(padding = padding, id = it.arguments?.getString("id")) }
@@ -1,12 +1,11 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.content.pm.PackageManager
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -26,10 +25,13 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@@ -50,12 +52,14 @@ import kotlinx.coroutines.launch
fun ConfigScreen( fun ConfigScreen(
viewModel: ConfigViewModel = hiltViewModel(), viewModel: ConfigViewModel = hiltViewModel(),
padding: PaddingValues, padding: PaddingValues,
focusRequester: FocusRequester,
navController: NavController, navController: NavController,
id : String? id : String?
) { ) {
val context = LocalContext.current val context = LocalContext.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null) val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null)
@@ -72,127 +76,155 @@ fun ConfigScreen(
} }
if(tunnel != null) { if(tunnel != null) {
Column( LazyColumn(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding) .padding(padding)
) { ) {
Row( item {
modifier = Modifier Row(
.fillMaxWidth() modifier = Modifier
.padding(horizontal = 20.dp, vertical = 7.dp), .fillMaxWidth()
verticalAlignment = Alignment.CenterVertically, .padding(horizontal = 20.dp, vertical = 7.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
OutlinedTextField(
value = tunnelName.value,
onValueChange = {
viewModel.onTunnelNameChange(it)
},
label = { Text(stringResource(id = R.string.tunnel_name)) },
maxLines = 1,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
keyboardController?.hide()
viewModel.onTunnelNameChange(tunnelName.value)
}
),
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(id = R.string.tunnel_all))
Switch(
checked = allApplications,
onCheckedChange = {
viewModel.onAllApplicationsChange(!allApplications)
}
)
}
if(!allApplications) {
Row(modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) { horizontalArrangement = Arrangement.SpaceBetween
Row(verticalAlignment = Alignment.CenterVertically, ) {
horizontalArrangement = Arrangement.SpaceBetween){ OutlinedTextField(
Text(stringResource(id = R.string.include)) modifier = Modifier.focusRequester(focusRequester),
Checkbox( value = tunnelName.value,
checked = include, onValueChange = {
onCheckedChange = { viewModel.onTunnelNameChange(it)
viewModel.onIncludeChange(!include) },
label = { Text(stringResource(id = R.string.tunnel_name)) },
maxLines = 1,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
keyboardController?.hide()
viewModel.onTunnelNameChange(tunnelName.value)
} }
) ),
} )
Row(verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween){
Text(stringResource(id = R.string.exclude))
Checkbox(
checked = !include,
onCheckedChange = {
viewModel.onIncludeChange(!include)
}
)
}
} }
LazyColumn(modifier = Modifier }
.fillMaxWidth() item {
.fillMaxHeight(.75f) Row(
.padding(horizontal = 14.dp, vertical = 7.dp), modifier = Modifier
verticalArrangement = Arrangement.Center, .fillMaxWidth()
horizontalAlignment = Alignment.Start) { .padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(id = R.string.tunnel_all))
Switch(
checked = allApplications,
onCheckedChange = {
viewModel.onAllApplicationsChange(!allApplications)
}
)
}
}
if (!allApplications) {
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(id = R.string.include))
Checkbox(
checked = include,
onCheckedChange = {
viewModel.onIncludeChange(!include)
}
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(id = R.string.exclude))
Checkbox(
checked = !include,
onCheckedChange = {
viewModel.onIncludeChange(!include)
}
)
}
}
}
items(packages) { pack -> items(packages) { pack ->
Row(verticalAlignment = Alignment.CenterVertically, Row(
horizontalArrangement = Arrangement.SpaceBetween) { verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(5.dp) modifier = Modifier.padding(5.dp)
) { ) {
val drawable = pack.applicationInfo?.loadIcon(context.packageManager) val drawable =
if(drawable != null) { pack.applicationInfo?.loadIcon(context.packageManager)
Image(painter = DrawablePainter(drawable), stringResource(id = R.string.icon), modifier = Modifier.size(50.dp, 50.dp)) if (drawable != null) {
Image(
painter = DrawablePainter(drawable),
stringResource(id = R.string.icon),
modifier = Modifier.size(50.dp, 50.dp)
)
} else { } else {
Icon(Icons.Rounded.Android, stringResource(id = R.string.edit), modifier = Modifier.size(50.dp, 50.dp)) Icon(
Icons.Rounded.Android,
stringResource(id = R.string.edit),
modifier = Modifier.size(50.dp, 50.dp)
)
} }
Text(pack.applicationInfo.loadLabel(context.packageManager).toString(), modifier = Modifier.padding(5.dp)) Text(
pack.applicationInfo.loadLabel(context.packageManager)
.toString(), modifier = Modifier.padding(5.dp)
)
} }
Checkbox( Checkbox(
checked = (checkedPackages.contains(pack.packageName)), checked = (checkedPackages.contains(pack.packageName)),
onCheckedChange = { onCheckedChange = {
if(it) viewModel.onAddCheckedPackage(pack.packageName) else viewModel.onRemoveCheckedPackage(pack.packageName) if (it) viewModel.onAddCheckedPackage(pack.packageName) else viewModel.onRemoveCheckedPackage(
pack.packageName
)
} }
) )
} }
} }
} }
} item {
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Button(onClick = { Button(onClick = {
scope.launch { scope.launch {
viewModel.onSaveAllChanges() viewModel.onSaveAllChanges()
Toast.makeText(context, context.resources.getString(R.string.config_changes_saved), Toast.LENGTH_LONG).show() Toast.makeText(
navController.navigate(Routes.Main.name) context,
context.resources.getString(R.string.config_changes_saved),
Toast.LENGTH_LONG
).show()
navController.navigate(Routes.Main.name)
}
}, Modifier.padding(25.dp)) {
Text(stringResource(id = R.string.save_changes))
} }
}, Modifier.padding(25.dp)) {
Text(stringResource(id = R.string.save_changes))
} }
} }
} }
@@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -43,6 +45,7 @@ fun DetailScreen(
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle() val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle()
val lastHandshake by viewModel.lastHandshake.collectAsStateWithLifecycle(emptyMap()) val lastHandshake by viewModel.lastHandshake.collectAsStateWithLifecycle(emptyMap())
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.getTunnelById(id) viewModel.getTunnelById(id)
} }
@@ -51,12 +54,14 @@ fun DetailScreen(
val interfaceKey = tunnel?.`interface`?.keyPair?.publicKey?.toBase64().toString() val interfaceKey = tunnel?.`interface`?.keyPair?.publicKey?.toBase64().toString()
val addresses = tunnel?.`interface`?.addresses!!.joinToString() val addresses = tunnel?.`interface`?.addresses!!.joinToString()
val dnsServers = tunnel?.`interface`?.dnsServers!!.joinToString() val dnsServers = tunnel?.`interface`?.dnsServers!!.joinToString()
val mtu = tunnel?.`interface`?.mtu?.get().toString() val optionalMtu = tunnel?.`interface`?.mtu
val mtu = if(optionalMtu?.isPresent == true) optionalMtu.get().toString() else "None"
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(padding) .padding(padding)
) { ) {
Row( Row(
@@ -92,7 +97,7 @@ fun DetailScreen(
tunnel?.peers?.forEach{ tunnel?.peers?.forEach{
val peerKey = it.publicKey.toBase64().toString() val peerKey = it.publicKey.toBase64().toString()
val allowedIps = it.allowedIps.joinToString() val allowedIps = it.allowedIps.joinToString()
val endpoint = it.endpoint.get().toString() val endpoint = if(it.endpoint.isPresent) it.endpoint.get().toString() else "None"
Text(stringResource(R.string.peer), fontWeight = FontWeight.Bold, fontSize = 20.sp) Text(stringResource(R.string.peer), fontWeight = FontWeight.Bold, fontSize = 20.sp)
Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic) Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic)
Text(text = peerKey, modifier = Modifier.clickable { Text(text = peerKey, modifier = Modifier.clickable {
@@ -3,7 +3,11 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint 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.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -14,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen import androidx.compose.material.icons.filled.FileOpen
@@ -22,6 +27,7 @@ import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Circle import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
@@ -43,11 +49,18 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
@@ -59,13 +72,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController import androidx.navigation.NavController
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.Routes import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed
import com.zaneschepke.wireguardautotunnel.ui.theme.mint import com.zaneschepke.wireguardautotunnel.ui.theme.mint
import com.zaneschepke.wireguardautotunnel.ui.theme.pinkRed
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@@ -78,6 +91,7 @@ fun MainScreen(
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val context = LocalContext.current val context = LocalContext.current
val isVisible = rememberSaveable { mutableStateOf(true) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
@@ -89,6 +103,23 @@ fun MainScreen(
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN) val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("") val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
// Nested scroll for control FAB
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// Hide FAB
if (available.y < -1) {
isVisible.value = false
}
// Show FAB
if (available.y > 1) {
isVisible.value = true
}
return Offset.Zero
}
}
}
LaunchedEffect(viewState.value) { LaunchedEffect(viewState.value) {
if (viewState.value.showSnackbarMessage) { if (viewState.value.showSnackbarMessage) {
val result = snackbarHostState.showSnackbar( val result = snackbarHostState.showSnackbar(
@@ -119,19 +150,25 @@ fun MainScreen(
}, },
floatingActionButtonPosition = FabPosition.End, floatingActionButtonPosition = FabPosition.End,
floatingActionButton = { floatingActionButton = {
FloatingActionButton( AnimatedVisibility(
modifier = Modifier.padding(bottom = 90.dp), visible = isVisible.value,
onClick = { enter = slideInVertically(initialOffsetY = { it * 2 }),
showBottomSheet = true exit = slideOutVertically(targetOffsetY = { it * 2 }),
},
containerColor = MaterialTheme.colorScheme.secondary,
shape = RoundedCornerShape(16.dp),
) { ) {
Icon( FloatingActionButton(
imageVector = Icons.Rounded.Add, modifier = Modifier.padding(bottom = 90.dp),
contentDescription = stringResource(id = R.string.add_tunnel), onClick = {
tint = Color.DarkGray, showBottomSheet = true
) },
containerColor = MaterialTheme.colorScheme.secondary,
shape = RoundedCornerShape(16.dp),
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(id = R.string.add_tunnel),
tint = Color.DarkGray,
)
}
} }
} }
) { ) {
@@ -204,15 +241,19 @@ fun MainScreen(
.padding(padding) .padding(padding)
) { ) {
LazyColumn(modifier = Modifier.fillMaxSize()) { LazyColumn(
items(tunnels.toList()) { tunnel -> modifier = Modifier.fillMaxSize()
.nestedScroll(nestedScrollConnection),
) {
itemsIndexed(tunnels.toList()) { index, tunnel ->
val focusRequester = FocusRequester();
RowListItem(leadingIcon = Icons.Rounded.Circle, RowListItem(leadingIcon = Icons.Rounded.Circle,
leadingIconColor = when (handshakeStatus) { leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) {
HandshakeStatus.HEALTHY -> mint HandshakeStatus.HEALTHY -> mint
HandshakeStatus.UNHEALTHY -> brickRed HandshakeStatus.UNHEALTHY -> brickRed
HandshakeStatus.NOT_STARTED -> Color.Gray HandshakeStatus.NOT_STARTED -> Color.Gray
HandshakeStatus.NEVER_CONNECTED -> brickRed HandshakeStatus.NEVER_CONNECTED -> brickRed
}, } else Color.Gray,
text = tunnel.name, text = tunnel.name,
onHold = { onHold = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName) { if (state == Tunnel.State.UP && tunnel.name == tunnelName) {
@@ -224,16 +265,24 @@ fun MainScreen(
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
selectedTunnel = tunnel; selectedTunnel = tunnel;
}, },
onClick = { navController.navigate("${Routes.Detail.name}/${tunnel.id}") }, onClick = {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
} else {
focusRequester.requestFocus()
}
},
rowButton = { rowButton = {
if (tunnel.id == selectedTunnel?.id) { if (tunnel.id == selectedTunnel?.id) {
Row() { Row {
IconButton(onClick = { IconButton(onClick = {
navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}") navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}")
}) { }) {
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit)) Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
} }
IconButton(onClick = { viewModel.onDelete(tunnel) }) { IconButton(
modifier = Modifier.focusable(),
onClick = { viewModel.onDelete(tunnel) }) {
Icon( Icon(
Icons.Rounded.Delete, Icons.Rounded.Delete,
stringResource(id = R.string.delete) stringResource(id = R.string.delete)
@@ -241,12 +290,64 @@ fun MainScreen(
} }
} }
} else { } else {
Switch( if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName), Row {
onCheckedChange = { checked -> IconButton(
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop() 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
)
)
} else {
navController.navigate("${Routes.Config.name}/${tunnel.id}")
}
}) {
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
)
)
} else {
viewModel.onDelete(tunnel)
}
}) {
Icon(
Icons.Rounded.Delete,
stringResource(id = R.string.delete)
)
}
Switch(
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
onCheckedChange = { checked ->
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
}
)
} }
) } else {
Switch(
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
onCheckedChange = { checked ->
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
}
)
}
} }
}) })
} }
@@ -86,6 +86,7 @@ class MainViewModel @Inject constructor(private val application : Application,
val setting = settings[0] val setting = settings[0]
setting.defaultTunnel = null setting.defaultTunnel = null
setting.isAutoTunnelEnabled = false setting.isAutoTunnelEnabled = false
setting.isAlwaysOnVpnEnabled = false
settingsRepo.save(setting) settingsRepo.save(setting)
} }
} }
@@ -143,7 +144,12 @@ class MainViewModel @Inject constructor(private val application : Application,
@SuppressLint("Range") @SuppressLint("Range")
private fun getFileName(context: Context, uri: Uri): String { private fun getFileName(context: Context, uri: Uri): String {
if (uri.scheme == "content") { 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 ?: return defaultConfigName()
cursor.use { cursor.use {
if(cursor.moveToFirst()) { if(cursor.moveToFirst()) {
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.Manifest import android.Manifest
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.provider.Settings import android.provider.Settings
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -12,12 +13,15 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Add
@@ -45,6 +49,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
@@ -62,6 +68,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted 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.service.tunnel.model.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.Routes import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
@@ -75,6 +82,7 @@ fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(), viewModel: SettingsViewModel = hiltViewModel(),
padding: PaddingValues, padding: PaddingValues,
navController: NavController, navController: NavController,
focusRequester: FocusRequester,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() } snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
) { ) {
@@ -90,7 +98,10 @@ fun SettingsScreen(
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf()) val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
val backgroundLocationState = val backgroundLocationState =
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION) rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
var currentText by remember { mutableStateOf("") } var currentText by remember { mutableStateOf("") }
val scrollState = rememberScrollState()
var isLocationServicesEnabled by remember { mutableStateOf(viewModel.checkLocationServicesEnabled())}
LaunchedEffect(viewState) { LaunchedEffect(viewState) {
if (viewState.showSnackbarMessage) { if (viewState.showSnackbarMessage) {
@@ -115,20 +126,32 @@ fun SettingsScreen(
} }
} }
if(!backgroundLocationState.status.isGranted) { fun openSettings() {
scope.launch {
val intentSettings =
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intentSettings.data =
Uri.fromParts("package", context.packageName, null)
context.startActivity(intentSettings)
}
}
if(!backgroundLocationState.status.isGranted && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
Column(horizontalAlignment = Alignment.CenterHorizontally, Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(scrollState)
.padding(padding)) { .padding(padding)) {
Icon(Icons.Rounded.LocationOff, contentDescription = stringResource(id = R.string.map), modifier = Modifier Icon(Icons.Rounded.LocationOff, contentDescription = stringResource(id = R.string.map), modifier = Modifier
.padding(30.dp) .padding(30.dp)
.size(128.dp)) .size(128.dp))
Text(stringResource(R.string.prominent_background_location_title), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 20.sp) Text(stringResource(R.string.prominent_background_location_title), textAlign = TextAlign.Center, 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) Text(stringResource(R.string.prominent_background_location_message), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp)
//Spacer(modifier = Modifier.weight(1f))
Row( Row(
modifier = Modifier modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
.fillMaxWidth()
.padding(10.dp) else Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(30.dp), .padding(30.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -139,14 +162,8 @@ fun SettingsScreen(
}) { }) {
Text(stringResource(id = R.string.no_thanks)) Text(stringResource(id = R.string.no_thanks))
} }
Button(onClick = { Button(modifier = Modifier.focusRequester(focusRequester), onClick = {
scope.launch { openSettings()
val intentSettings =
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intentSettings.data =
Uri.fromParts("package", context.packageName, null)
context.startActivity(intentSettings)
}
}) { }) {
Text(stringResource(id = R.string.turn_on)) Text(stringResource(id = R.string.turn_on))
} }
@@ -155,6 +172,30 @@ fun SettingsScreen(
return return
} }
if(!fineLocationState.status.isGranted) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
Text(
stringResource(id = R.string.precise_location_message),
textAlign = TextAlign.Center,
modifier = Modifier.padding(15.dp),
fontStyle = FontStyle.Italic
)
Button(modifier = Modifier.focusRequester(focusRequester),onClick = {
fineLocationState.launchPermissionRequest()
}) {
Text(stringResource(id = R.string.request))
}
}
return
}
if (tunnels.isEmpty()) { if (tunnels.isEmpty()) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -172,12 +213,48 @@ fun SettingsScreen(
} }
return return
} }
if(!isLocationServicesEnabled && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
Text(
stringResource(id = R.string.location_services_not_detected),
textAlign = TextAlign.Center,
modifier = Modifier.padding(15.dp),
fontStyle = FontStyle.Italic
)
Button(modifier = Modifier.focusRequester(focusRequester), onClick = {
val locationServicesEnabled = viewModel.checkLocationServicesEnabled()
isLocationServicesEnabled = locationServicesEnabled
if(!locationServicesEnabled) {
scope.launch {
viewModel.showSnackBarMessage(context.getString(R.string.detecting_location_services_disabled))
}
}
}) {
Text(stringResource(id = R.string.check_again))
}
}
return
}
val screenPadding = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 5.dp else 15.dp
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
.fillMaxHeight(.85f)
.fillMaxWidth()
.verticalScroll(scrollState)
.clickable(indication = null, interactionSource = interactionSource) {
focusManager.clearFocus()
}
.padding(padding) else Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(scrollState)
.clickable(indication = null, interactionSource = interactionSource) { .clickable(indication = null, interactionSource = interactionSource) {
focusManager.clearFocus() focusManager.clearFocus()
} }
@@ -186,12 +263,14 @@ fun SettingsScreen(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(14.dp), .padding(screenPadding),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text(stringResource(R.string.enable_auto_tunnel)) Text(stringResource(R.string.enable_auto_tunnel))
Switch( Switch(
modifier = Modifier.focusRequester(focusRequester),
enabled = !settings.isAlwaysOnVpnEnabled,
checked = settings.isAutoTunnelEnabled, checked = settings.isAutoTunnelEnabled,
onCheckedChange = { onCheckedChange = {
scope.launch { scope.launch {
@@ -203,17 +282,19 @@ fun SettingsScreen(
Text( Text(
stringResource(id = R.string.select_tunnel), stringResource(id = R.string.select_tunnel),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.padding(15.dp, bottom = 5.dp, top = 5.dp) modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
) )
ExposedDropdownMenuBox( ExposedDropdownMenuBox(
expanded = expanded, expanded = expanded,
onExpandedChange = { onExpandedChange = {
if(!settings.isAutoTunnelEnabled) { if(!(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)) {
expanded = !expanded }}, expanded = !expanded }},
modifier = Modifier.padding(start = 15.dp, top = 5.dp, bottom = 10.dp), modifier = Modifier.padding(start = 15.dp, top = 5.dp, bottom = 10.dp).clickable {
expanded = !expanded
},
) { ) {
TextField( TextField(
enabled = !settings.isAutoTunnelEnabled, enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
value = settings.defaultTunnel?.let { value = settings.defaultTunnel?.let {
TunnelConfig.from(it).name } TunnelConfig.from(it).name }
?: "", ?: "",
@@ -249,27 +330,27 @@ fun SettingsScreen(
Text( Text(
stringResource(R.string.trusted_ssid), stringResource(R.string.trusted_ssid),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.padding(15.dp, bottom = 5.dp, top = 5.dp) modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
) )
FlowRow( FlowRow(
modifier = Modifier.padding(15.dp), modifier = Modifier.padding(screenPadding),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically verticalArrangement = Arrangement.SpaceEvenly
) { ) {
trustedSSIDs.forEach { ssid -> trustedSSIDs.forEach { ssid ->
ClickableIconButton(onIconClick = { ClickableIconButton(onIconClick = {
scope.launch { scope.launch {
viewModel.onDeleteTrustedSSID(ssid) viewModel.onDeleteTrustedSSID(ssid)
} }
}, text = ssid, icon = Icons.Filled.Close, enabled = !settings.isAutoTunnelEnabled) }, text = ssid, icon = Icons.Filled.Close, enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled))
} }
} }
OutlinedTextField( OutlinedTextField(
enabled = !settings.isAutoTunnelEnabled, enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
value = currentText, value = currentText,
onValueChange = { currentText = it }, onValueChange = { currentText = it },
label = { Text(stringResource(R.string.add_trusted_ssid)) }, label = { Text(stringResource(R.string.add_trusted_ssid)) },
modifier = Modifier.padding(start = 15.dp, top = 5.dp), modifier = Modifier.padding(start = screenPadding, top = 5.dp),
maxLines = 1, maxLines = 1,
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None, capitalization = KeyboardCapitalization.None,
@@ -295,13 +376,13 @@ fun SettingsScreen(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(14.dp), .padding(screenPadding),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text(stringResource(R.string.tunnel_mobile_data)) Text(stringResource(R.string.tunnel_mobile_data))
Switch( Switch(
enabled = !settings.isAutoTunnelEnabled, enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
checked = settings.isTunnelOnMobileDataEnabled, checked = settings.isTunnelOnMobileDataEnabled,
onCheckedChange = { onCheckedChange = {
scope.launch { scope.launch {
@@ -310,6 +391,24 @@ fun SettingsScreen(
} }
) )
} }
Row(
modifier = Modifier
.fillMaxWidth()
.padding(screenPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.always_on_vpn_support))
Switch(
enabled = !settings.isAutoTunnelEnabled,
checked = settings.isAlwaysOnVpnEnabled,
onCheckedChange = {
scope.launch {
viewModel.onToggleAlwaysOnVPN()
}
}
)
}
} }
} }
@@ -1,6 +1,8 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.app.Application import android.app.Application
import android.content.Context
import android.location.LocationManager
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
@@ -17,6 +19,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SettingsViewModel @Inject constructor(private val application : Application, class SettingsViewModel @Inject constructor(private val application : Application,
private val tunnelRepo : Repository<TunnelConfig>, private val settingsRepo : Repository<Settings> private val tunnelRepo : Repository<TunnelConfig>, private val settingsRepo : Repository<Settings>
@@ -31,6 +34,7 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
val viewState get() = _viewState.asStateFlow() val viewState get() = _viewState.asStateFlow()
init { init {
checkLocationServicesEnabled()
viewModelScope.launch { viewModelScope.launch {
settingsRepo.itemFlow.collect { settingsRepo.itemFlow.collect {
val settings = it.first() val settings = it.first()
@@ -69,7 +73,7 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
suspend fun toggleAutoTunnel() { suspend fun toggleAutoTunnel() {
if(_settings.value.defaultTunnel.isNullOrEmpty() && !_settings.value.isAutoTunnelEnabled) { if(_settings.value.defaultTunnel.isNullOrEmpty() && !_settings.value.isAutoTunnelEnabled) {
showSnackBarMessage("Please select a tunnel first") showSnackBarMessage(application.getString(R.string.select_tunnel_message))
return return
} }
if(_settings.value.isAutoTunnelEnabled) { if(_settings.value.isAutoTunnelEnabled) {
@@ -99,8 +103,7 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
} }
} }
} }
suspend fun showSnackBarMessage(message : String) {
private suspend fun showSnackBarMessage(message : String) {
_viewState.emit(_viewState.value.copy( _viewState.emit(_viewState.value.copy(
showSnackbarMessage = true, showSnackbarMessage = true,
snackbarMessage = message, snackbarMessage = message,
@@ -118,4 +121,20 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
showSnackbarMessage = false showSnackbarMessage = false
)) ))
} }
suspend fun onToggleAlwaysOnVPN() {
if(_settings.value.defaultTunnel != null) {
_settings.emit(
_settings.value.copy(isAlwaysOnVpnEnabled = !_settings.value.isAlwaysOnVpnEnabled)
)
settingsRepo.save(_settings.value)
} else {
showSnackBarMessage(application.getString(R.string.select_tunnel_message))
}
}
fun checkLocationServicesEnabled() : Boolean {
val locationManager =
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
}
} }
@@ -1,8 +1,10 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support package com.zaneschepke.wireguardautotunnel.ui.screens.support
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -11,12 +13,18 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -29,7 +37,7 @@ import androidx.compose.ui.unit.sp
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
@Composable @Composable
fun SupportScreen(padding : PaddingValues) { fun SupportScreen(padding : PaddingValues, focusRequester: FocusRequester) {
val context = LocalContext.current val context = LocalContext.current
@@ -43,6 +51,8 @@ fun SupportScreen(padding : PaddingValues) {
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState())
.focusable()
.padding(padding)) { .padding(padding)) {
Text(stringResource(R.string.support_text), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp) Text(stringResource(R.string.support_text), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp)
Row( Row(
@@ -57,7 +67,7 @@ fun SupportScreen(padding : PaddingValues) {
}) { }) {
Icon(imageVector = ImageVector.vectorResource(R.drawable.discord), "Discord") Icon(imageVector = ImageVector.vectorResource(R.drawable.discord), "Discord")
} }
IconButton(onClick = { IconButton(modifier = Modifier.focusRequester(focusRequester),onClick = {
openWebPage(context.resources.getString(R.string.github_url)) openWebPage(context.resources.getString(R.string.github_url))
}) { }) {
Icon(imageVector = ImageVector.vectorResource(R.drawable.github), "Github") Icon(imageVector = ImageVector.vectorResource(R.drawable.github), "Github")
+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>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_banner_background"/>
<foreground android:drawable="@mipmap/ic_banner_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_banner_background">#121212</color>
</resources>
+9
View File
@@ -77,4 +77,13 @@
<string name="failed_connection_to">Failed connection to -</string> <string name="failed_connection_to">Failed connection to -</string>
<string name="initial_connection_failure_message">Attempting to connect to server after 30 seconds of no response.</string> <string name="initial_connection_failure_message">Attempting to connect to server after 30 seconds of no response.</string>
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string> <string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
<string name="always_on_vpn_support">Enable Always-On VPN support</string>
<string name="select_tunnel_message">Please select a tunnel first</string>
<string name="location_services_not_detected">Unable to detect Location Services which are required for this feature. Please enable Location Services.</string>
<string name="check_again">Check again</string>
<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> </resources>
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 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 { 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("org.jetbrains.kotlin.android") version "1.8.22" apply false
id("com.google.dagger.hilt.android") version "2.44" apply false id("com.google.dagger.hilt.android") version "2.44" apply false
kotlin("plugin.serialization") version "1.8.22" 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 #Mon Apr 24 22:46:45 EDT 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists