Compare commits

..

1 Commits

Author SHA1 Message Date
Zane Schepke f63ce8abcd 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-17 17:31:52 -04:00
54 changed files with 420 additions and 1208 deletions
+3 -14
View File
@@ -13,17 +13,9 @@ WG Tunnel
[![Google Play](https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
[![Fire TV](https://img.shields.io/badge/fire%20tv-fc3b2d?style=for-the-badge&logo=amazon%20fire%20tv&logoColor=white)](https://www.amazon.com/gp/product/B0CFGGL7WK)
</span>
<span align="center">
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/N4N8NMJN2)
</span>
<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.
@@ -37,8 +29,8 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard
<p float="center">
<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="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="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" />
</p>
<span align="left">
@@ -51,10 +43,7 @@ The inspiration for this app came from the inconvenience of constantly having to
* Add tunnels via .conf file
* Auto connect to VPN based on Wi-Fi SSID
* Split tunneling by application with search
* Always-on VPN for Android support
* Quick tile support for vpn toggling
* Dynamic shortcuts support for automation integration
* Split tunneling by application
* Configurable Trusted Network list
* Optional auto connect on mobile data
* Automatic service restart after reboot
+13 -10
View File
@@ -5,25 +5,25 @@ plugins {
id("org.jetbrains.kotlin.android")
kotlin("kapt")
id("com.google.dagger.hilt.android")
id("io.objectbox")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
id("org.jetbrains.kotlin.plugin.serialization")
id("io.objectbox")
}
android {
namespace = "com.zaneschepke.wireguardautotunnel"
compileSdk = 34
compileSdk = 33
val versionMajor = 2
val versionMinor = 4
val versionPatch = 4
val versionMinor = 1
val versionPatch = 1
val versionBuild = 0
defaultConfig {
applicationId = "com.zaneschepke.wireguardautotunnel"
minSdk = 28
targetSdk = 34
minSdk = 29
targetSdk = 33
versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
@@ -83,13 +83,13 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-test-manifest")
//wireguard tunnel
implementation("com.wireguard.android:tunnel:1.0.20230706")
implementation("com.wireguard.android:tunnel:1.0.20230427")
//logging
implementation("com.jakewharton.timber:timber:5.0.1")
// compose navigation
implementation("androidx.navigation:navigation-compose:2.7.1")
implementation("androidx.navigation:navigation-compose:2.6.0")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
// hilt
@@ -110,7 +110,7 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
//icons
implementation("androidx.compose.material:material-icons-extended:1.5.0")
implementation("androidx.compose.material:material-icons-extended:1.4.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
@@ -122,7 +122,10 @@ dependencies {
implementation("com.google.firebase:firebase-analytics-ktx")
//barcode scanning
implementation("com.google.android.gms:play-services-code-scanner:16.1.0")
implementation("com.google.android.gms:play-services-code-scanner:16.0.0")
}
kapt {
+1 -6
View File
@@ -31,7 +31,7 @@
},
{
"id": "2:8887605597748372702",
"lastPropertyId": "9:4468844863383145378",
"lastPropertyId": "8:4981008812459251156",
"name": "Settings",
"properties": [
{
@@ -59,11 +59,6 @@
"id": "6:3370284381040192129",
"name": "defaultTunnel",
"type": 9
},
{
"id": "9:4468844863383145378",
"name": "isAlwaysOnVpnEnabled",
"type": 1
}
],
"relations": []
+6 -2
View File
@@ -59,6 +59,11 @@
"id": "6:3370284381040192129",
"name": "defaultTunnel",
"type": 9
},
{
"id": "8:4981008812459251156",
"name": "showProminentDisclosure",
"type": 1
}
],
"relations": []
@@ -86,8 +91,7 @@
7555225587864607050,
969146862000617878,
5057486545428188436,
2814640993034665120,
4981008812459251156
2814640993034665120
],
"retiredRelationUids": [],
"version": 1
+3 -50
View File
@@ -1,36 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
android:maxSdkVersion="30"
tools:ignore="LeanbackUsesWifi" />
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<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_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.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>
<!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!--start service on boot permission-->
<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>
<intent>
<action android:name="android.intent.action.MAIN" />
@@ -42,7 +29,6 @@
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:banner="@mipmap/ic_banner"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
@@ -54,54 +40,24 @@
android:theme="@style/Theme.WireguardAutoTunnel">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:finishOnTaskLaunch="true"
android:theme="@android:style/Theme.NoDisplay"
android:name=".service.shortcut.ShortcutsActivity"/>
<service
android:name=".service.foreground.ForegroundService"
android:enabled="true"
android:foregroundServiceType="remoteMessaging"
android:exported="false">
</service>
<service
android:exported="true"
android:name=".service.tile.TunnelControlTile"
android:icon="@drawable/shield"
android:label="WG Tunnel"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".service.foreground.WireGuardTunnelService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:enabled="true"
android:persistent="true"
android:foregroundServiceType="remoteMessaging"
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
android:name=".service.foreground.WireGuardConnectivityWatcherService"
android:enabled="true"
android:stopWithTask="false"
android:persistent="true"
android:foregroundServiceType="location"
android:permission=""
android:exported="false">
</service>
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
@@ -114,8 +70,5 @@
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui"/>
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="true" />
</application>
</manifest>
@@ -1,6 +0,0 @@
package com.zaneschepke.wireguardautotunnel
object Constants {
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L;
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L;
}
@@ -1,10 +1,6 @@
package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.mlkit.common.MlKit
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.HiltAndroidApp
@@ -19,21 +15,7 @@ class WireGuardAutoTunnel : Application() {
override fun onCreate() {
super.onCreate()
if(BuildConfig.DEBUG) {
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false);
Timber.plant(Timber.DebugTree())
}
try {
MlKit.initialize(this)
} catch (e : Exception) {
Timber.e(e.message)
}
if(BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
settingsRepo.init()
}
companion object {
fun isRunningOnAndroidTv(context : Context) : Boolean {
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
}
}
}
@@ -7,6 +7,7 @@ import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.barcode.QRScanner
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -1,5 +1,7 @@
package com.zaneschepke.wireguardautotunnel.module
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.barcode.QRScanner
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
@@ -10,6 +12,7 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent
import dagger.hilt.android.scopes.ServiceScoped
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ServiceComponent::class)
@@ -3,12 +3,16 @@ package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -21,13 +25,18 @@ class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(SupervisorJob()).launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
ServiceTracker.actionOnService(
Action.START, context,
WireGuardConnectivityWatcherService::class.java,
mapOf(context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!)
)
}
}
} finally {
@@ -3,12 +3,15 @@ package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -20,15 +23,29 @@ class NotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var settingsRepo : Repository<Settings>
override fun onReceive(context: Context, intent: Intent?) {
CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(SupervisorJob()).launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if (setting.defaultTunnel != null) {
ServiceManager.stopVpnService(context)
ServiceTracker.actionOnService(
Action.STOP, context,
WireGuardTunnelService::class.java,
mapOf(
context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!
)
)
delay(1000)
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
ServiceTracker.actionOnService(
Action.START, context,
WireGuardTunnelService::class.java,
mapOf(
context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!
)
)
}
}
} finally {
@@ -2,6 +2,5 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
enum class Action {
START,
START_FOREGROUND,
STOP
}
@@ -22,12 +22,8 @@ open class ForegroundService : Service() {
val action = intent.action
Timber.d("using an intent with action $action")
when (action) {
Action.START.name, Action.START_FOREGROUND.name -> startService(intent.extras)
Action.START.name -> startService(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 {
@@ -1,110 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.ActivityManager
import android.app.Service
import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.Intent
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import com.zaneschepke.wireguardautotunnel.R
object ServiceManager {
@Suppress("DEPRECATION")
private // Deprecated for third party Services.
fun <T> Context.isServiceRunning(service: Class<T>) =
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.getRunningServices(Integer.MAX_VALUE)
.any { it.service.className == service.name }
fun <T : Service> getServiceState(context: Context, cls : Class<T>): ServiceState {
val isServiceRunning = context.isServiceRunning(cls)
return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
}
private fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
val intent = Intent(context, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
it.putExtra(k, v)
}
}
intent.component?.javaClass
try {
when(action) {
Action.START_FOREGROUND -> {
context.startForegroundService(intent)
}
Action.START -> {
context.startService(intent)
}
Action.STOP -> context.startService(intent)
}
} catch (e : Exception) {
e.message?.let { Firebase.crashlytics.log(it) }
}
}
fun startVpnService(context : Context, tunnelConfig : String) {
actionOnService(
Action.START,
context,
WireGuardTunnelService::class.java,
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
}
fun stopVpnService(context : Context) {
actionOnService(
Action.STOP,
context,
WireGuardTunnelService::class.java
)
}
fun startVpnServiceForeground(context : Context, tunnelConfig : String) {
actionOnService(
Action.START_FOREGROUND,
context,
WireGuardTunnelService::class.java,
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
}
private fun startWatcherServiceForeground(context : Context, tunnelConfig : String) {
actionOnService(
Action.START, context,
WireGuardConnectivityWatcherService::class.java, mapOf(context.
getString(R.string.tunnel_extras_key) to
tunnelConfig))
}
fun startWatcherService(context : Context, tunnelConfig : String) {
actionOnService(
Action.START, context,
WireGuardConnectivityWatcherService::class.java, mapOf(context.
getString(R.string.tunnel_extras_key) to
tunnelConfig))
}
fun stopWatcherService(context : Context) {
actionOnService(
Action.STOP, context,
WireGuardConnectivityWatcherService::class.java)
}
fun toggleWatcherService(context: Context, tunnelConfig : String) {
when(getServiceState( context,
WireGuardConnectivityWatcherService::class.java,)) {
ServiceState.STARTED -> stopWatcherService(context)
ServiceState.STOPPED -> startWatcherService(context, tunnelConfig)
}
}
fun toggleWatcherServiceForeground(context: Context, tunnelConfig : String) {
when(getServiceState( context,
WireGuardConnectivityWatcherService::class.java,)) {
ServiceState.STARTED -> stopWatcherService(context)
ServiceState.STOPPED -> startWatcherServiceForeground(context, tunnelConfig)
}
}
}
@@ -0,0 +1,56 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.ActivityManager
import android.app.Application
import android.app.Service
import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.Intent
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
object ServiceTracker {
@Suppress("DEPRECATION")
private // Deprecated for third party Services.
fun <T> Context.isServiceRunning(service: Class<T>) =
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.getRunningServices(Integer.MAX_VALUE)
.any { it.service.className == service.name }
fun <T : Service> getServiceState(context: Context, cls : Class<T>): ServiceState {
val isServiceRunning = context.isServiceRunning(cls)
return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
}
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
val intent = Intent(application, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
it.putExtra(k, v)
}
}
intent.component?.javaClass
try {
application.startService(intent)
} catch (e : Exception) {
e.message?.let { Firebase.crashlytics.log(it) }
}
}
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
val intent = Intent(context, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
it.putExtra(k, v)
}
}
intent.component?.javaClass
try {
context.startService(intent)
} catch (e : Exception) {
e.message?.let { Firebase.crashlytics.log(it) }
}
}
}
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.AlarmManager
import android.app.Application
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
@@ -8,7 +9,6 @@ import android.os.Bundle
import android.os.PowerManager
import android.os.SystemClock
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
@@ -20,9 +20,8 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -47,36 +46,30 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
@Inject
lateinit var vpnService : VpnService
private var isWifiConnected = false;
private var isMobileDataConnected = false;
private var currentNetworkSSID = "";
private lateinit var watcherJob : Job;
private lateinit var setting : Settings
private lateinit var tunnelConfig: 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 val tag = this.javaClass.name;
override fun onCreate() {
super.onCreate()
CoroutineScope(Dispatchers.Main).launch {
launchWatcherNotification()
}
}
override fun startService(extras: Bundle?) {
super.startService(extras)
launchWatcherNotification()
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
if (tunnelId != null) {
this.tunnelConfig = tunnelId
this.tunnelId = tunnelId
}
// we need this lock so our service gets not affected by Doze Mode
initWakeLock()
cancelWatcherJob()
if(this::tunnelConfig.isInitialized) {
launchWatcherNotification()
if(this::tunnelId.isInitialized) {
startWatcherJob()
} else {
stopService(extras)
@@ -91,6 +84,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
}
cancelWatcherJob()
stopVPN()
stopSelf()
}
@@ -129,22 +123,19 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
private fun startWatcherJob() {
watcherJob = CoroutineScope(Dispatchers.IO).launch {
watcherJob = CoroutineScope(SupervisorJob()).launch {
val settings = settingsRepo.getAll();
if(!settings.isNullOrEmpty()) {
setting = settings[0]
}
launch {
CoroutineScope(watcherJob).launch {
watchForWifiConnectivityChanges()
}
if(setting.isTunnelOnMobileDataEnabled) {
launch {
CoroutineScope(watcherJob).launch {
watchForMobileDataConnectivityChanges()
}
}
launch {
manageVpn()
}
}
}
@@ -158,9 +149,17 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
is NetworkStatus.CapabilitiesChanged -> {
isMobileDataConnected = true
Timber.d("Mobile data capabilities changed")
if(!disconnecting && !connecting) {
if(!isWifiConnected && setting.isTunnelOnMobileDataEnabled
&& vpnService.getState() == Tunnel.State.DOWN)
startVPN()
}
}
is NetworkStatus.Unavailable -> {
isMobileDataConnected = false
if(!disconnecting && !connecting) {
if(!isWifiConnected && vpnService.getState() == Tunnel.State.UP) stopVPN()
}
Timber.d("Lost mobile data connection")
}
}
@@ -177,37 +176,61 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Wifi capabilities changed")
isWifiConnected = true
currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: "";
if (!connecting && !disconnecting) {
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 -> {
isWifiConnected = false
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 suspend fun manageVpn() {
while(true) {
if(setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected
&& vpnService.getState() == Tunnel.State.DOWN) {
ServiceManager.startVpnService(this, tunnelConfig)
} else if(!setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
vpnService.getState() == Tunnel.State.UP) {
ServiceManager.stopVpnService(this)
} else if(isWifiConnected &&
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
(vpnService.getState() != Tunnel.State.UP)) {
ServiceManager.startVpnService(this, tunnelConfig)
} else if((isWifiConnected &&
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(vpnService.getState() == Tunnel.State.UP)) {
ServiceManager.stopVpnService(this)
}
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
private fun startVPN() {
if(!connecting) {
connecting = true
ServiceTracker.actionOnService(
Action.START,
this.applicationContext as Application,
WireGuardTunnelService::class.java,
mapOf(getString(R.string.tunnel_extras_key) to tunnelId))
connecting = false
}
}
private fun stopVPN() {
if(!disconnecting) {
disconnecting = true
ServiceTracker.actionOnService(
Action.STOP,
this.applicationContext as Application,
WireGuardTunnelService::class.java
)
disconnecting = false
}
}
}
@@ -5,16 +5,15 @@ import android.content.Intent
import android.os.Bundle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
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.SupervisorJob
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -27,9 +26,6 @@ class WireGuardTunnelService : ForegroundService() {
@Inject
lateinit var vpnService : VpnService
@Inject
lateinit var settingsRepo: Repository<Settings>
@Inject
lateinit var notificationService : NotificationService
@@ -37,19 +33,11 @@ class WireGuardTunnelService : ForegroundService() {
private var tunnelName : String = ""
override fun onCreate() {
super.onCreate()
CoroutineScope(Dispatchers.Main).launch {
launchVpnStartingNotification()
}
}
override fun startService(extras : Bundle?) {
super.startService(extras)
launchVpnStartingNotification()
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
cancelJob()
job = CoroutineScope(Dispatchers.IO).launch {
job = CoroutineScope(SupervisorJob()).launch {
if(tunnelConfigString != null) {
try {
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
@@ -60,16 +48,7 @@ class WireGuardTunnelService : ForegroundService() {
stopService(extras)
}
} else {
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)
}
}
Timber.e("Tunnel config null")
}
}
CoroutineScope(job).launch {
@@ -83,7 +62,6 @@ class WireGuardTunnelService : ForegroundService() {
if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message))
didShowFailedHandshakeNotification = true
didShowConnected = false
}
}
HandshakeStatus.HEALTHY -> {
@@ -96,7 +74,6 @@ class WireGuardTunnelService : ForegroundService() {
if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message))
didShowFailedHandshakeNotification = true
didShowConnected = false
}
}
}
@@ -106,7 +83,7 @@ class WireGuardTunnelService : ForegroundService() {
override fun stopService(extras : Bundle?) {
super.stopService(extras)
CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(Dispatchers.IO).launch() {
vpnService.stopTunnel()
}
cancelJob()
@@ -125,18 +102,6 @@ class WireGuardTunnelService : ForegroundService() {
super.startForeground(foregroundId, notification)
}
private fun launchVpnStartingNotification() {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
title = getString(R.string.vpn_starting),
onGoing = false,
showTimestamp = true,
description = getString(R.string.attempt_connection)
)
super.startForeground(foregroundId, notification)
}
private fun launchVpnConnectionFailedNotification(message : String) {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
@@ -28,6 +28,7 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
object : ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO
) {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
@@ -44,7 +45,6 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
@@ -67,8 +67,6 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
}
val request = NetworkRequest.Builder()
.addTransportType(networkCapability)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
@@ -79,8 +77,8 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
var ssid : String? = getWifiNameFromCapabilities(networkCapabilities)
if((Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R)) {
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
@@ -91,15 +89,14 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
companion object {
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val info: WifiInfo
if (networkCapabilities.transportInfo is WifiInfo) {
info = networkCapabilities.transportInfo as WifiInfo
return info.ssid
}
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities) : String? {
val info : WifiInfo
if(networkCapabilities.transportInfo is WifiInfo) {
info = networkCapabilities.transportInfo as WifiInfo
} else {
return null
}
return null
return info.ssid
}
}
}
@@ -1,28 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ShortcutsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(intent.getStringExtra(ShortcutsManager.CLASS_NAME_EXTRA_KEY)
.equals(WireGuardTunnelService::class.java.name)) {
intent.getStringExtra(getString(R.string.tunnel_extras_key))?.let {
ServiceManager.toggleWatcherService(this, it)
}
when(intent.action){
Action.STOP.name -> ServiceManager.stopVpnService(this)
Action.START.name -> intent.getStringExtra(getString(R.string.tunnel_extras_key))
?.let { ServiceManager.startVpnService(this, it) }
}
}
finish()
}
}
@@ -1,73 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.content.Context
import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
object ShortcutsManager {
private const val SHORT_LABEL_MAX_SIZE = 10;
private const val LONG_LABEL_MAX_SIZE = 25;
private const val APPEND_ON = " On";
private const val APPEND_OFF = " Off"
const val CLASS_NAME_EXTRA_KEY = "className"
private fun createAndPushShortcut(context : Context, intent : Intent, id : String, shortLabel : String,
longLabel : String, drawable : Int ) {
val shortcut = ShortcutInfoCompat.Builder(context, id)
.setShortLabel(shortLabel)
.setLongLabel(longLabel)
.setIcon(IconCompat.createWithResource(context, drawable))
.setIntent(intent)
.build()
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)
}
fun createTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
createAndPushShortcut(context,
createTunnelOnIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
tunnelConfig.id.toString() + APPEND_ON,
tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
R.drawable.vpn_on
)
createAndPushShortcut(context,
createTunnelOffIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
tunnelConfig.id.toString() + APPEND_OFF,
tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
R.drawable.vpn_off
)
}
fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON,
tunnelConfig.id.toString() + APPEND_OFF ))
}
private fun createTunnelOnIntent(context: Context, extras : Map<String,String>) : Intent {
return Intent(context, ShortcutsActivity::class.java).also {
it.action = Action.START.name
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
extras.forEach {(k, v) ->
it.putExtra(k, v)
}
}
}
private fun createTunnelOffIntent(context : Context, extras : Map<String,String>) : Intent {
return Intent(context, ShortcutsActivity::class.java).also {
it.action = Action.STOP.name
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
extras.forEach {(k, v) ->
it.putExtra(k, v)
}
}
}
}
@@ -1,142 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.tile
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.ServiceManager
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) {
ServiceManager.stopVpnService(this@TunnelControlTile)
} else {
ServiceManager.startVpnServiceForeground(this@TunnelControlTile, 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 attemptWatcherServiceToggle(tunnelConfig : String) {
scope.launch {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if(setting.isAutoTunnelEnabled) {
ServiceManager.toggleWatcherServiceForeground(this@TunnelControlTile, 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();
}
}
}
@@ -5,7 +5,6 @@ import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.crypto.Key
import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import kotlinx.coroutines.CoroutineScope
@@ -30,7 +29,6 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
override val tunnelName get() = _tunnelName.asStateFlow()
private val _state = MutableSharedFlow<Tunnel.State>(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
replay = 1)
private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1,
@@ -105,7 +103,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
_handshakeStatus.emit(HandshakeStatus.NOT_STARTED)
}
if(neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
neverHadHandshakeCounter += 10
neverHadHandshakeCounter++
}
return@forEach
}
@@ -116,7 +114,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
}
}
_lastHandshake.emit(handshakeMap)
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
delay(1000)
}
}
}
@@ -10,6 +10,5 @@ data class Settings(
var isAutoTunnelEnabled : Boolean = false,
var isTunnelOnMobileDataEnabled : Boolean = false,
var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
var defaultTunnel : String? = null,
var isAlwaysOnVpnEnabled : Boolean = false,
var defaultTunnel : String? = null
)
@@ -6,7 +6,6 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.KeyEvent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
@@ -23,9 +22,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
@@ -44,8 +40,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import java.lang.IllegalStateException
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@@ -57,8 +51,6 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberAnimatedNavController()
val focusRequester = remember { FocusRequester() }
WireguardAutoTunnelTheme {
TransparentSystemBars()
@@ -88,25 +80,7 @@ class MainActivity : AppCompatActivity() {
} else requestNotificationPermission()
}
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
}
},
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) },
bottomBar = if (vpnIntent == null && notificationPermissionState.status.isGranted) {
{ BottomNavBar(navController, Routes.navItems) }
} else {
@@ -173,7 +147,7 @@ class MainActivity : AppCompatActivity() {
fadeIn(animationSpec = tween(1000))
}
}
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController, focusRequester = focusRequester) }
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController) }
composable(Routes.Support.name, enterTransition = {
when (initialState.destination.route) {
Routes.Settings.name, Routes.Main.name ->
@@ -186,10 +160,10 @@ class MainActivity : AppCompatActivity() {
fadeIn(animationSpec = tween(1000))
}
}
}) { SupportScreen(padding = padding, focusRequester) }
}) { SupportScreen(padding = padding) }
composable("${Routes.Config.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(1000))
}) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"), focusRequester = focusRequester)}
}) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"))}
composable("${Routes.Detail.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(1000))
}) { DetailScreen(padding = padding, id = it.arguments?.getString("id")) }
@@ -1,80 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun SearchBar(
onQuery : (queryString : String) -> Unit
) {
// Immediately update and keep track of query from text field changes.
var query: String by rememberSaveable { mutableStateOf("") }
var showClearIcon by rememberSaveable { mutableStateOf(false) }
if (query.isEmpty()) {
showClearIcon = false
} else if (query.isNotEmpty()) {
showClearIcon = true
}
TextField(
value = query,
onValueChange = { onQueryChanged ->
// If user makes changes to text, immediately updated it.
query = onQueryChanged
onQuery(onQueryChanged)
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
tint = MaterialTheme.colorScheme.onBackground,
contentDescription = stringResource(id = R.string.search_icon)
)
},
trailingIcon = {
if (showClearIcon) {
IconButton(onClick = { query = "" }) {
Icon(
imageVector = Icons.Rounded.Clear,
tint = MaterialTheme.colorScheme.onBackground,
contentDescription = stringResource(id = R.string.clear_icon)
)
}
}
},
maxLines = 1,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
),
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
textStyle = MaterialTheme.typography.bodySmall,
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
modifier = Modifier
.fillMaxWidth()
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape)
)
}
@@ -3,8 +3,10 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -28,8 +30,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
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.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@@ -43,7 +43,6 @@ import androidx.navigation.NavController
import com.google.accompanist.drawablepainter.DrawablePainter
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
import kotlinx.coroutines.launch
@OptIn(ExperimentalComposeUiApi::class)
@@ -51,14 +50,12 @@ import kotlinx.coroutines.launch
fun ConfigScreen(
viewModel: ConfigViewModel = hiltViewModel(),
padding: PaddingValues,
focusRequester: FocusRequester,
navController: NavController,
id : String?
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val scope = rememberCoroutineScope()
val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null)
@@ -70,170 +67,132 @@ fun ConfigScreen(
LaunchedEffect(Unit) {
viewModel.getTunnelById(id)
viewModel.emitQueriedPackages("")
viewModel.emitAllInternetCapablePackages()
viewModel.emitCurrentPackageConfigurations(id)
}
if(tunnel != null) {
LazyColumn(
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
OutlinedTextField(
modifier = Modifier.focusRequester(focusRequester),
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
) {
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)
}
),
)
}
item {
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)
}
)
}
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) {
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)
}
)
if(!allApplications) {
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)
}
)
)
}
Row(verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween){
Text(stringResource(id = R.string.exclude))
Checkbox(
checked = !include,
onCheckedChange = {
viewModel.onIncludeChange(!include)
}
}
}
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) {
SearchBar(viewModel::emitQueriedPackages);
}
)
}
}
LazyColumn(modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(.75f)
.padding(horizontal = 14.dp, vertical = 7.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start) {
items(packages) { pack ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(5.dp)
) {
val drawable =
pack.applicationInfo?.loadIcon(context.packageManager)
if (drawable != null) {
Image(
painter = DrawablePainter(drawable),
stringResource(id = R.string.icon),
modifier = Modifier.size(50.dp, 50.dp)
)
val drawable = pack.applicationInfo?.loadIcon(context.packageManager)
if(drawable != null) {
Image(painter = DrawablePainter(drawable), stringResource(id = R.string.icon), modifier = Modifier.size(50.dp, 50.dp))
} 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(
checked = (checkedPackages.contains(pack.packageName)),
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(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Button(onClick = {
scope.launch {
viewModel.onSaveAllChanges()
Toast.makeText(
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))
}
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Button(onClick = {
scope.launch {
viewModel.onSaveAllChanges()
Toast.makeText(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))
}
}
}
@@ -8,14 +8,12 @@ import android.os.Build
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -98,12 +96,8 @@ class ConfigViewModel @Inject constructor(private val application : Application,
}
}
fun emitQueriedPackages(query : String) {
viewModelScope.launch {
_packages.emit(getAllInternetCapablePackages().filter {
it.packageName.contains(query)
})
}
suspend fun emitAllInternetCapablePackages() {
_packages.emit(getAllInternetCapablePackages())
}
private fun getAllInternetCapablePackages() : List<PackageInfo> {
@@ -9,8 +9,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -45,7 +43,6 @@ fun DetailScreen(
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle()
val lastHandshake by viewModel.lastHandshake.collectAsStateWithLifecycle(emptyMap())
LaunchedEffect(Unit) {
viewModel.getTunnelById(id)
}
@@ -54,14 +51,12 @@ fun DetailScreen(
val interfaceKey = tunnel?.`interface`?.keyPair?.publicKey?.toBase64().toString()
val addresses = tunnel?.`interface`?.addresses!!.joinToString()
val dnsServers = tunnel?.`interface`?.dnsServers!!.joinToString()
val optionalMtu = tunnel?.`interface`?.mtu
val mtu = if(optionalMtu?.isPresent == true) optionalMtu.get().toString() else "None"
val mtu = tunnel?.`interface`?.mtu?.get().toString()
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(padding)
) {
Row(
@@ -97,7 +92,7 @@ fun DetailScreen(
tunnel?.peers?.forEach{
val peerKey = it.publicKey.toBase64().toString()
val allowedIps = it.allowedIps.joinToString()
val endpoint = if(it.endpoint.isPresent) it.endpoint.get().toString() else "None"
val endpoint = it.endpoint.get().toString()
Text(stringResource(R.string.peer), fontWeight = FontWeight.Bold, fontSize = 20.sp)
Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic)
Text(text = peerKey, modifier = Modifier.clickable {
@@ -1,14 +1,9 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
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.focusable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -18,7 +13,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen
@@ -27,7 +22,6 @@ import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
@@ -49,18 +43,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.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.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
@@ -72,13 +59,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed
import com.zaneschepke.wireguardautotunnel.ui.theme.mint
import com.zaneschepke.wireguardautotunnel.ui.theme.pinkRed
import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@@ -91,7 +78,6 @@ fun MainScreen(
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
val isVisible = rememberSaveable { mutableStateOf(true) }
val scope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState()
@@ -103,24 +89,6 @@ fun MainScreen(
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
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) {
if (viewState.value.showSnackbarMessage) {
val result = snackbarHostState.showSnackbar(
@@ -136,9 +104,11 @@ fun MainScreen(
}
val pickFileLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
result.data?.data?.let { viewModel.onTunnelFileSelected(it) }
ActivityResultContracts.GetContent()
) { file ->
if (file != null) {
viewModel.onTunnelFileSelected(file)
}
}
Scaffold(
@@ -149,25 +119,19 @@ fun MainScreen(
},
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
AnimatedVisibility(
visible = isVisible.value,
enter = slideInVertically(initialOffsetY = { it * 2 }),
exit = slideOutVertically(targetOffsetY = { it * 2 }),
FloatingActionButton(
modifier = Modifier.padding(bottom = 90.dp),
onClick = {
showBottomSheet = true
},
containerColor = MaterialTheme.colorScheme.secondary,
shape = RoundedCornerShape(16.dp),
) {
FloatingActionButton(
modifier = Modifier.padding(bottom = 90.dp),
onClick = {
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,
)
}
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(id = R.string.add_tunnel),
tint = Color.DarkGray,
)
}
}
) {
@@ -195,11 +159,7 @@ fun MainScreen(
.fillMaxWidth()
.clickable {
showBottomSheet = false
val fileSelectionIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
pickFileLauncher.launch(fileSelectionIntent)
pickFileLauncher.launch("*/*")
}
.padding(10.dp)
) {
@@ -244,20 +204,15 @@ fun MainScreen(
.padding(padding)
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection),
) {
itemsIndexed(tunnels.toList()) { index, tunnel ->
val focusRequester = FocusRequester();
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(tunnels.toList()) { tunnel ->
RowListItem(leadingIcon = Icons.Rounded.Circle,
leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) {
leadingIconColor = when (handshakeStatus) {
HandshakeStatus.HEALTHY -> mint
HandshakeStatus.UNHEALTHY -> brickRed
HandshakeStatus.NOT_STARTED -> Color.Gray
HandshakeStatus.NEVER_CONNECTED -> brickRed
} else Color.Gray,
},
text = tunnel.name,
onHold = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName) {
@@ -269,24 +224,16 @@ fun MainScreen(
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
selectedTunnel = tunnel;
},
onClick = {
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
} else {
focusRequester.requestFocus()
}
},
onClick = { navController.navigate("${Routes.Detail.name}/${tunnel.id}") },
rowButton = {
if (tunnel.id == selectedTunnel?.id) {
Row {
Row() {
IconButton(onClick = {
navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}")
}) {
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
}
IconButton(
modifier = Modifier.focusable(),
onClick = { viewModel.onDelete(tunnel) }) {
IconButton(onClick = { viewModel.onDelete(tunnel) }) {
Icon(
Icons.Rounded.Delete,
stringResource(id = R.string.delete)
@@ -294,64 +241,12 @@ fun MainScreen(
}
}
} else {
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
Row {
IconButton(
modifier = Modifier.focusRequester(focusRequester),
onClick = {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
}) {
Icon(Icons.Rounded.Info, "Info")
}
IconButton(onClick = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
scope.launch {
viewModel.showSnackBarMessage(
context.resources.getString(
R.string.turn_off_tunnel
)
)
} 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()
}
)
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()
}
)
}
)
}
})
}
@@ -12,10 +12,11 @@ import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.barcode.CodeScanner
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager
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
@@ -63,44 +64,48 @@ class MainViewModel @Inject constructor(private val application : Application,
}
private fun validateWatcherServiceState(settings: Settings) {
val watcherState = ServiceManager.getServiceState(application.applicationContext, WireGuardConnectivityWatcherService::class.java)
val watcherState = ServiceTracker.getServiceState(application, WireGuardConnectivityWatcherService::class.java)
if(settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
ServiceManager.startWatcherService(application.applicationContext, settings.defaultTunnel!!)
startWatcherService(settings.defaultTunnel!!)
}
}
private fun startWatcherService(tunnel : String) {
ServiceTracker.actionOnService(
Action.START, application,
WireGuardConnectivityWatcherService::class.java,
mapOf(application.resources.getString(R.string.tunnel_extras_key) to tunnel))
}
fun onDelete(tunnel : TunnelConfig) {
viewModelScope.launch {
if(tunnelRepo.count() == 1L) {
ServiceManager.stopWatcherService(application.applicationContext)
ServiceTracker.actionOnService( Action.STOP, application, WireGuardConnectivityWatcherService::class.java)
val settings = settingsRepo.getAll()
if(!settings.isNullOrEmpty()) {
val setting = settings[0]
setting.defaultTunnel = null
setting.isAutoTunnelEnabled = false
setting.isAlwaysOnVpnEnabled = false
settingsRepo.save(setting)
}
}
tunnelRepo.delete(tunnel)
ShortcutsManager.removeTunnelShortcuts(application.applicationContext, tunnel)
}
}
fun onTunnelStart(tunnelConfig : TunnelConfig) = viewModelScope.launch {
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
ServiceTracker.actionOnService( Action.START, application, WireGuardTunnelService::class.java,
mapOf(application.resources.getString(R.string.tunnel_extras_key) to tunnelConfig.toString()))
}
fun onTunnelStop() {
ServiceManager.stopVpnService(application.applicationContext)
ServiceTracker.actionOnService( Action.STOP, application, WireGuardTunnelService::class.java)
}
suspend fun onTunnelQRSelected() {
codeScanner.scan().collect {
if(!it.isNullOrEmpty() && it.contains(application.resources.getString(R.string.config_validation))) {
val tunnelConfig = TunnelConfig(name = defaultConfigName(), wgQuick = it)
saveTunnel(tunnelConfig)
tunnelRepo.save(TunnelConfig(name = defaultConfigName(), wgQuick = it))
} else if(!it.isNullOrEmpty() && it.contains(application.resources.getString(R.string.barcode_downloading))) {
showSnackBarMessage(application.resources.getString(R.string.barcode_downloading_message))
} else {
@@ -124,7 +129,9 @@ class MainViewModel @Inject constructor(private val application : Application,
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
val config = Config.parse(bufferReader)
val tunnelName = getNameFromFileName(fileName)
saveTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
viewModelScope.launch {
tunnelRepo.save(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
}
stream.close()
} catch(_: BadConfigException) {
viewModelScope.launch {
@@ -133,22 +140,10 @@ class MainViewModel @Inject constructor(private val application : Application,
}
}
private fun saveTunnel(tunnelConfig : TunnelConfig) {
viewModelScope.launch {
tunnelRepo.save(tunnelConfig)
ShortcutsManager.createTunnelShortcuts(application.applicationContext, tunnelConfig)
}
}
@SuppressLint("Range")
private fun getFileName(context: Context, uri: Uri): String {
if (uri.scheme == "content") {
val cursor = try {
context.contentResolver.query(uri, null, null, null, null)
} catch (e : Exception) {
Timber.d("Exception getting config name")
null
}
val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor ?: return defaultConfigName()
cursor.use {
if(cursor.moveToFirst()) {
@@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.Manifest
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -13,15 +12,12 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Add
@@ -49,8 +45,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
@@ -68,7 +62,6 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
@@ -82,7 +75,6 @@ fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(),
padding: PaddingValues,
navController: NavController,
focusRequester: FocusRequester,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
) {
@@ -98,10 +90,7 @@ fun SettingsScreen(
val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf())
val backgroundLocationState =
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
var currentText by remember { mutableStateOf("") }
val scrollState = rememberScrollState()
var isLocationServicesEnabled by remember { mutableStateOf(viewModel.checkLocationServicesEnabled())}
LaunchedEffect(viewState) {
if (viewState.showSnackbarMessage) {
@@ -126,32 +115,20 @@ fun SettingsScreen(
}
}
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) {
if(!backgroundLocationState.status.isGranted) {
Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(padding)) {
Icon(Icons.Rounded.LocationOff, contentDescription = stringResource(id = R.string.map), modifier = Modifier
.padding(30.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_message), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp)
//Spacer(modifier = Modifier.weight(1f))
Row(
modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
.fillMaxWidth()
.padding(10.dp) else Modifier
modifier = Modifier
.fillMaxWidth()
.padding(30.dp),
verticalAlignment = Alignment.CenterVertically,
@@ -162,8 +139,14 @@ fun SettingsScreen(
}) {
Text(stringResource(id = R.string.no_thanks))
}
Button(modifier = Modifier.focusRequester(focusRequester), onClick = {
openSettings()
Button(onClick = {
scope.launch {
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))
}
@@ -172,30 +155,6 @@ fun SettingsScreen(
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()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -213,48 +172,12 @@ fun SettingsScreen(
}
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(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
.fillMaxHeight(.85f)
.fillMaxWidth()
.verticalScroll(scrollState)
.clickable(indication = null, interactionSource = interactionSource) {
focusManager.clearFocus()
}
.padding(padding) else Modifier
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.clickable(indication = null, interactionSource = interactionSource) {
focusManager.clearFocus()
}
@@ -263,14 +186,12 @@ fun SettingsScreen(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(screenPadding),
.padding(14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.enable_auto_tunnel))
Switch(
modifier = Modifier.focusRequester(focusRequester),
enabled = !settings.isAlwaysOnVpnEnabled,
checked = settings.isAutoTunnelEnabled,
onCheckedChange = {
scope.launch {
@@ -282,19 +203,17 @@ fun SettingsScreen(
Text(
stringResource(id = R.string.select_tunnel),
textAlign = TextAlign.Center,
modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
modifier = Modifier.padding(15.dp, bottom = 5.dp, top = 5.dp)
)
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
if(!(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)) {
if(!settings.isAutoTunnelEnabled) {
expanded = !expanded }},
modifier = Modifier.padding(start = 15.dp, top = 5.dp, bottom = 10.dp).clickable {
expanded = !expanded
},
modifier = Modifier.padding(start = 15.dp, top = 5.dp, bottom = 10.dp),
) {
TextField(
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
enabled = !settings.isAutoTunnelEnabled,
value = settings.defaultTunnel?.let {
TunnelConfig.from(it).name }
?: "",
@@ -330,27 +249,27 @@ fun SettingsScreen(
Text(
stringResource(R.string.trusted_ssid),
textAlign = TextAlign.Center,
modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
modifier = Modifier.padding(15.dp, bottom = 5.dp, top = 5.dp)
)
FlowRow(
modifier = Modifier.padding(screenPadding),
modifier = Modifier.padding(15.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.SpaceEvenly
verticalAlignment = Alignment.CenterVertically
) {
trustedSSIDs.forEach { ssid ->
ClickableIconButton(onIconClick = {
scope.launch {
viewModel.onDeleteTrustedSSID(ssid)
}
}, text = ssid, icon = Icons.Filled.Close, enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled))
}, text = ssid, icon = Icons.Filled.Close, enabled = !settings.isAutoTunnelEnabled)
}
}
OutlinedTextField(
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
enabled = !settings.isAutoTunnelEnabled,
value = currentText,
onValueChange = { currentText = it },
label = { Text(stringResource(R.string.add_trusted_ssid)) },
modifier = Modifier.padding(start = screenPadding, top = 5.dp),
modifier = Modifier.padding(start = 15.dp, top = 5.dp),
maxLines = 1,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
@@ -376,13 +295,13 @@ fun SettingsScreen(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(screenPadding),
.padding(14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(R.string.tunnel_mobile_data))
Switch(
enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled),
enabled = !settings.isAutoTunnelEnabled,
checked = settings.isTunnelOnMobileDataEnabled,
onCheckedChange = {
scope.launch {
@@ -391,24 +310,6 @@ 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,13 +1,13 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.app.Application
import android.content.Context
import android.location.LocationManager
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.ViewState
@@ -17,7 +17,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(private val application : Application,
private val tunnelRepo : Repository<TunnelConfig>, private val settingsRepo : Repository<Settings>
@@ -32,7 +31,6 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
val viewState get() = _viewState.asStateFlow()
init {
checkLocationServicesEnabled()
viewModelScope.launch {
settingsRepo.itemFlow.collect {
val settings = it.first()
@@ -71,23 +69,38 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
suspend fun toggleAutoTunnel() {
if(_settings.value.defaultTunnel.isNullOrEmpty() && !_settings.value.isAutoTunnelEnabled) {
showSnackBarMessage(application.getString(R.string.select_tunnel_message))
showSnackBarMessage("Please select a tunnel first")
return
}
if(_settings.value.isAutoTunnelEnabled) {
ServiceManager.stopWatcherService(application)
actionOnWatcherService(Action.STOP)
} else {
if(_settings.value.defaultTunnel != null) {
val defaultTunnel = _settings.value.defaultTunnel
ServiceManager.startWatcherService(application, defaultTunnel!!)
}
actionOnWatcherService(Action.START)
}
settingsRepo.save(_settings.value.copy(
isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled
))
}
suspend fun showSnackBarMessage(message : String) {
private fun actionOnWatcherService(action : Action) {
when(action) {
Action.START -> {
if(_settings.value.defaultTunnel != null) {
val defaultTunnel = _settings.value.defaultTunnel
ServiceTracker.actionOnService(
action, application,
WireGuardConnectivityWatcherService::class.java,
mapOf(application.resources.getString(R.string.tunnel_extras_key) to defaultTunnel.toString()))
}
}
Action.STOP -> {
ServiceTracker.actionOnService( Action.STOP, application,
WireGuardConnectivityWatcherService::class.java)
}
}
}
private suspend fun showSnackBarMessage(message : String) {
_viewState.emit(_viewState.value.copy(
showSnackbarMessage = true,
snackbarMessage = message,
@@ -105,20 +118,4 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
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,10 +1,8 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -13,18 +11,12 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -37,7 +29,7 @@ import androidx.compose.ui.unit.sp
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun SupportScreen(padding : PaddingValues, focusRequester: FocusRequester) {
fun SupportScreen(padding : PaddingValues) {
val context = LocalContext.current
@@ -51,8 +43,6 @@ fun SupportScreen(padding : PaddingValues, focusRequester: FocusRequester) {
verticalArrangement = Arrangement.Top,
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.focusable()
.padding(padding)) {
Text(stringResource(R.string.support_text), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp)
Row(
@@ -67,7 +57,7 @@ fun SupportScreen(padding : PaddingValues, focusRequester: FocusRequester) {
}) {
Icon(imageVector = ImageVector.vectorResource(R.drawable.discord), "Discord")
}
IconButton(modifier = Modifier.focusRequester(focusRequester),onClick = {
IconButton(onClick = {
openWebPage(context.resources.getString(R.string.github_url))
}) {
Icon(imageVector = ImageVector.vectorResource(R.drawable.github), "Github")
-5
View File
@@ -1,5 +0,0 @@
<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>
-5
View File
@@ -1,5 +0,0 @@
<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="M20.83,18H21v-4h2v-4H12.83L20.83,18zM19.78,22.61l1.41,-1.41L2.81,2.81L1.39,4.22l2.59,2.59C2.2,7.85 1,9.79 1,12c0,3.31 2.69,6 6,6c2.21,0 4.15,-1.2 5.18,-2.99L19.78,22.61zM8.99,11.82C9,11.88 9,11.94 9,12c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2s0.9,-2 2,-2c0.06,0 0.12,0 0.18,0.01L8.99,11.82z"/>
</vector>
-5
View File
@@ -1,5 +0,0 @@
<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.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
</vector>
@@ -1,5 +0,0 @@
<?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.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_banner_background">#121212</color>
</resources>
-14
View File
@@ -77,18 +77,4 @@
<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="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>
<string name="hint_search_packages">Search packages</string>
<string name="clear_icon">Clear Icon</string>
<string name="search_icon">Search Icon</string>
<string name="attempt_connection">Attempting connection..</string>
<string name="vpn_starting">VPN Starting</string>
</resources>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

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