Compare commits

...

28 Commits

Author SHA1 Message Date
Zane Schepke ca3f3fd439 fix: file selection AndroidTV
Closes #35 Closes #34
2023-09-28 09:52:35 -04:00
Zane Schepke 235170508b docs: Update README.md 2023-09-26 17:06:12 -04:00
Zane Schepke 11aea3f1c4 fix: trusted SSIDs bug
Fixes bugs where trusted SSIDs were getting whitespace added every time they were saved to the DB.

Fixes file explorer bug on AndroidTV where the app crashes if they do not have a file explorer installed on the device.
2023-09-25 06:49:07 -04:00
Zane Schepke 7fbc51af4c feat: tunnel on ethernet
Adds settings feature to enable auto tunneling on ethernet connections.

Add alphabetical sorting for split tunneling packages

Fix bug where search was not searching by displayed label but by package name

Other refactoring and improvements.

Closes #29 Closes #27 Closes #28
2023-09-24 07:40:17 -04:00
Zane Schepke 1714618f0c refactor: bump release to 3.0.0
Bump release to 3.0.0 for F-Droid tag referencing for auto update
2023-09-16 10:46:30 -04:00
Zane Schepke 7cb798a111 refactor: change to static version codes for F-Droid auto-update 2023-09-16 02:06:55 -04:00
Zane Schepke f8bc264f30 refactor: change qrcode scanner 2023-09-15 14:51:38 -04:00
Zane Schepke 2174c3f48c fix: f-droid pipelines issues 2023-09-15 13:36:27 -04:00
Zane Schepke 413b9a37df fix: f-droid build warnings 2023-09-15 13:03:35 -04:00
Zane Schepke 6c30c6bae6 refactor: change flavor to fdroid 2023-09-15 11:31:16 -04:00
Zane Schepke 3c5aff31aa refactor: objectbox to room
Migrated app from using ObjectBox library for db to Room as ObjectBox is not FLOSS compliant.

Migrated app to using version catalog for managing dependancies.

Added floss build flavor for F-Droid and general build flavor for all other builds. Floss build excludes google analytics and crashlytics.

Other performance improvements and refactors.
2023-09-15 07:24:19 -04:00
Zane Schepke 991d1224ab chore: add fastlane for F-Droid 2023-09-13 03:10:40 -04:00
Zane Schepke 69b07eec6f feat: support Android 8
Add support for Android 8.

Fix shortcuts bug where it was toggling auto-tunneling without setting enabled.

Fix shortcuts name not updating with config edits.

Bump versions.

Closes #25
2023-09-12 12:48:54 -04:00
Zane Schepke e81066f508 fix: FireTV file selection bug
Fixes bug where FireTV devices were unable to launch a proper file browser to select tunnel configs.
2023-09-11 11:15:44 -04:00
Zane Schepke 64bb9f3b82 fix: foreground service start crashes
Attempt to fix startForegrounService causing crashes on some devices by not meeting the 5 second notification rule. Add notification to onCreate of services.

Limit startForeground to only be called where it is truly necessary in the TileService to allow starting the VPN while app is not running.

Attempt to manually initialize mlkit for QR code scanning to remediate some crashes caused by config scanning.
2023-09-10 00:23:23 -04:00
Zane Schepke c1b560e822 chore: update README.md 2023-09-08 12:43:41 -04:00
Zane Schepke 14fe5821cc Merge branch 'main' of github.com:zaneschepke/wgtunnel 2023-09-04 05:52:27 -04:00
Zane Schepke 9d9b7bebca fix: foreground service crash on older devices
Fixes a bug where older device take a longer time to launch the foreground service and connect to the VPN. Combined with a delayed launch of foreground notification until VPN connection is confirmed, this would break foreground service,s 5 second notification rule.

Fixed by adding a new attempting connection notification to launch on vpn initial connection attempt.
2023-09-04 05:52:15 -04:00
Zane Schepke 12d1ccc084 chore: update README.md 2023-09-04 02:51:05 -04:00
Zane Schepke 20cc2c09b0 feat: dynamic shortcuts and package search
Add dynamic on and off shortcuts for each new tunnel added. Shortcuts will toggle auto tunneling to allow manual override.

Add package search for split tunneling.

Add tunnel toggling from tile even while app is closed and auto tunneling is disabled.

Refactor and code cleanup.

Closes #23 Closes #21
2023-09-04 02:42:49 -04:00
Zane Schepke eeccc71469 fix: tile auto tunnel toggle and wifi internet connection check
Change tile toggle behavior to also toggle auto tunnel service to allow user temporary override and re-enablement of auto tunnel from the tile.

Add tunnel name to tile.

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

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

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

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

Optimize imports.

Bump versions.
2023-08-11 20:21:28 -04:00
Zane Schepke 2abf681d17 feat: support Android 9
Added support for Android 9 by updating permission checks and wifi SSID logic.

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

Bump wireguard-android library version.

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

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

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

Update AndroidTV banner
2023-07-31 23:38:51 -04:00
81 changed files with 1757 additions and 1121 deletions
+16 -11
View File
@@ -2,34 +2,37 @@
WG Tunnel
</h1>
<span align="center">
<div align="center">
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Discord Chat](https://img.shields.io/discord/1108285024631001111.svg)](https://discord.gg/rbRRNh6H7V)
</span>
</div>
<span align="center">
<div align="center">
[![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)
[![F-Droid](https://img.shields.io/static/v1?style=for-the-badge&message=F-Droid&color=1976D2&logo=F-Droid&logoColor=FFFFFF&label=)](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
</span>
<span align="center">
</div>
<div align="center">
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/N4N8NMJN2)
</span>
</div>
<span align="left">
<div 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.
</span>
</div>
<span align="center">
<div align="center">
## Screenshots
@@ -40,7 +43,7 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard
<img label="Support" style="padding-left:25px" src="asset/support_screen.png" width="200" />
</p>
<span align="left">
<div align="left">
## Inspiration
@@ -50,8 +53,10 @@ 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
* Split tunneling by application with search
* Always-on VPN for Android support
* Quick tile support for vpn toggling
* Dynamic shortcuts support for automation integration
* Configurable Trusted Network list
* Optional auto connect on mobile data
* Automatic service restart after reboot
+76 -63
View File
@@ -1,31 +1,25 @@
val rExtra = rootProject.extra
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
kotlin("kapt")
id("com.google.dagger.hilt.android")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt.android)
id("org.jetbrains.kotlin.plugin.serialization")
id("io.objectbox")
alias(libs.plugins.ksp)
}
android {
namespace = "com.zaneschepke.wireguardautotunnel"
compileSdk = 34
val versionMajor = 2
val versionMinor = 3
val versionPatch = 0
val versionBuild = 0
defaultConfig {
applicationId = "com.zaneschepke.wireguardautotunnel"
minSdk = 29
minSdk = 26
targetSdk = 34
versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
versionCode = 30003
versionName = "3.0.3"
multiDexEnabled = true
resourceConfigurations.addAll(listOf("en"))
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -36,12 +30,30 @@ android {
buildTypes {
release {
isDebuggable = false
isMinifyEnabled = false
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
debug {
isDebuggable = true
}
}
flavorDimensions.add("type")
productFlavors {
create("fdroid") {
dimension = "type"
}
create("general") {
dimension = "type"
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle))
{
apply(plugin = "com.google.gms.google-services")
apply(plugin = "com.google.firebase.crashlytics")
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
@@ -52,9 +64,11 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.8"
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
}
packaging {
resources {
@@ -63,68 +77,67 @@ android {
}
}
val generalImplementation by configurations
dependencies {
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.activity:activity-compose:1.7.2")
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3:1.1.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat)
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
//test
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest)
//wireguard tunnel
implementation("com.wireguard.android:tunnel:1.0.20230427")
//wg
implementation(libs.tunnel)
//logging
implementation("com.jakewharton.timber:timber:5.0.1")
implementation(libs.timber)
// compose navigation
implementation("androidx.navigation:navigation-compose:2.6.0")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose)
// hilt
implementation("com.google.dagger:hilt-android:${rExtra.get("hiltVersion")}")
kapt("com.google.dagger:hilt-android-compiler:${rExtra.get("hiltVersion")}")
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
//accompanist
implementation("com.google.accompanist:accompanist-systemuicontroller:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-permissions:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-flowlayout:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-navigation-animation:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-drawablepainter:${rExtra.get("accompanistVersion")}")
implementation(libs.accompanist.systemuicontroller)
implementation(libs.accompanist.permissions)
implementation(libs.accompanist.flowlayout)
implementation(libs.accompanist.navigation.animation)
implementation(libs.accompanist.drawablepainter)
//db
implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}")
//room
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
//lifecycle
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
implementation(libs.lifecycle.runtime.compose)
//icons
implementation("androidx.compose.material:material-icons-extended:1.4.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
implementation(libs.material.icons.extended)
//serialization
implementation(libs.kotlinx.serialization.json)
//firebase crashlytics
implementation(platform("com.google.firebase:firebase-bom:32.0.0"))
implementation("com.google.firebase:firebase-crashlytics-ktx")
implementation("com.google.firebase:firebase-analytics-ktx")
generalImplementation(platform(libs.firebase.bom))
generalImplementation(libs.google.firebase.crashlytics.ktx)
generalImplementation(libs.google.firebase.analytics.ktx)
//barcode scanning
implementation("com.google.android.gms:play-services-code-scanner:16.0.0")
}
kapt {
correctErrorTypes = true
implementation(libs.zxing.android.embedded)
implementation(libs.zxing.core)
}
-99
View File
@@ -1,99 +0,0 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:2692736974585027589",
"lastPropertyId": "15:5057486545428188436",
"name": "TunnelConfig",
"properties": [
{
"id": "1:1985347930017457084",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "12:2409068226744965585",
"name": "name",
"indexId": "1:4811206443952699137",
"type": 9,
"flags": 34848
},
{
"id": "13:8987443291286312275",
"name": "wgQuick",
"type": 9
}
],
"relations": []
},
{
"id": "2:8887605597748372702",
"lastPropertyId": "9:4468844863383145378",
"name": "Settings",
"properties": [
{
"id": "1:7485739868216068651",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:5814013113141456749",
"name": "isAutoTunnelEnabled",
"type": 1
},
{
"id": "4:5645665441196906014",
"name": "trustedNetworkSSIDs",
"type": 30
},
{
"id": "5:4989886999117763881",
"name": "isTunnelOnMobileDataEnabled",
"type": 1
},
{
"id": "6:3370284381040192129",
"name": "defaultTunnel",
"type": 9
},
{
"id": "9:4468844863383145378",
"name": "isAlwaysOnVpnEnabled",
"type": 1
}
],
"relations": []
}
],
"lastEntityId": "2:8887605597748372702",
"lastIndexId": "1:4811206443952699137",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [
1763475292291320186,
6483820955437198310,
8323071516033820771,
5904440563612311217,
1408037976996390989,
7737847485212546994,
8215616901775229364,
8021610768066328637,
6174306582797008721,
2175939938544485767,
7555225587864607050,
969146862000617878,
5057486545428188436,
2814640993034665120,
4981008812459251156
],
"retiredRelationUids": [],
"version": 1
}
-94
View File
@@ -1,94 +0,0 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:2692736974585027589",
"lastPropertyId": "15:5057486545428188436",
"name": "TunnelConfig",
"properties": [
{
"id": "1:1985347930017457084",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "12:2409068226744965585",
"name": "name",
"indexId": "1:4811206443952699137",
"type": 9,
"flags": 34848
},
{
"id": "13:8987443291286312275",
"name": "wgQuick",
"type": 9
}
],
"relations": []
},
{
"id": "2:8887605597748372702",
"lastPropertyId": "8:4981008812459251156",
"name": "Settings",
"properties": [
{
"id": "1:7485739868216068651",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:5814013113141456749",
"name": "isAutoTunnelEnabled",
"type": 1
},
{
"id": "4:5645665441196906014",
"name": "trustedNetworkSSIDs",
"type": 30
},
{
"id": "5:4989886999117763881",
"name": "isTunnelOnMobileDataEnabled",
"type": 1
},
{
"id": "6:3370284381040192129",
"name": "defaultTunnel",
"type": 9
}
],
"relations": []
}
],
"lastEntityId": "2:8887605597748372702",
"lastIndexId": "1:4811206443952699137",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [
1763475292291320186,
6483820955437198310,
8323071516033820771,
5904440563612311217,
1408037976996390989,
7737847485212546994,
8215616901775229364,
8021610768066328637,
6174306582797008721,
2175939938544485767,
7555225587864607050,
969146862000617878,
5057486545428188436,
2814640993034665120,
4981008812459251156
],
"retiredRelationUids": [],
"version": 1
}
+27 -6
View File
@@ -56,18 +56,44 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
</intent-filter>
</activity>
<activity
android:name=".ui.CaptureActivityPortrait"
android:screenOrientation="fullSensor"
android:stateNotNeeded="true"
android:theme="@style/zxing_CaptureTheme"
android:windowSoftInputMode="stateAlwaysHidden" />
<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>
@@ -80,6 +106,7 @@
android:name=".service.foreground.WireGuardConnectivityWatcherService"
android:enabled="true"
android:stopWithTask="false"
android:persistent="true"
android:foregroundServiceType="location"
android:permission=""
android:exported="false">
@@ -91,11 +118,5 @@
</intent-filter>
</receiver>
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
<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>
@@ -0,0 +1,17 @@
package com.zaneschepke.wireguardautotunnel
object Constants {
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L
const val SNACKBAR_DELAY = 3000L
const val TOGGLE_TUNNEL_DELAY = 500L
const val FADE_IN_ANIMATION_DURATION = 1000
const val SLIDE_IN_ANIMATION_DURATION = 500
const val SLIDE_IN_TRANSITION_OFFSET = 1000
const val VALID_FILE_EXTENSION = ".conf"
const val URI_CONTENT_SCHEME = "content"
const val URI_PACKAGE_SCHEME = "package"
const val ALLOWED_FILE_TYPES = "*/*"
const val FILES_SHOW_ADVANCED = "android.content.extra.SHOW_ADVANCED"
const val ANDROID_TV_STUBS = "com.google.android.tv.frameworkpackagestubs"
}
@@ -1,25 +1,24 @@
package com.zaneschepke.wireguardautotunnel
import android.app.Application
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import android.content.Context
import android.content.pm.PackageManager
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
import javax.inject.Inject
@HiltAndroidApp
class WireGuardAutoTunnel : Application() {
@Inject
lateinit var settingsRepo : Repository<Settings>
override fun onCreate() {
super.onCreate()
if(BuildConfig.DEBUG) {
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false);
Timber.plant(Timber.DebugTree())
}
settingsRepo.init()
}
companion object {
fun isRunningOnAndroidTv(context : Context) : Boolean {
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
}
}
}
@@ -1,40 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.MyObjectBox
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.objectbox.Box
import io.objectbox.BoxStore
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class BoxModule {
@Provides
@Singleton
fun provideBoxStore(@ApplicationContext context : Context) : BoxStore {
return MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
}
@Provides
@Singleton
fun provideBoxForSettings(store : BoxStore) : Box<Settings> {
return store.boxFor(Settings::class.java)
}
@Provides
@Singleton
fun provideBoxForTunnels(store : BoxStore) : Box<TunnelConfig> {
return store.boxFor(TunnelConfig::class.java)
}
}
@@ -0,0 +1,25 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import androidx.room.Room
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context : Context) : AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java, context.getString(R.string.db_name)
).build()
}
}
@@ -1,25 +1,27 @@
package com.zaneschepke.wireguardautotunnel.module
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.repository.SettingsBox
import com.zaneschepke.wireguardautotunnel.repository.TunnelBox
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.Binds
import com.zaneschepke.wireguardautotunnel.repository.AppDatabase
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
class RepositoryModule {
@Binds
@Singleton
abstract fun provideSettingsRepository(settingsBox: SettingsBox) : Repository<Settings>
@Provides
fun provideSettingsRepository(appDatabase: AppDatabase) : SettingsDoa {
return appDatabase.settingDao()
}
@Binds
@Singleton
abstract fun provideTunnelRepository(tunnelBox: TunnelBox) : Repository<TunnelConfig>
@Provides
fun provideTunnelConfigRepository(appDatabase: AppDatabase) : TunnelConfigDao {
return appDatabase.tunnelConfigDoa()
}
}
@@ -1,40 +0,0 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScanner
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.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
class ScannerModule {
@ViewModelScoped
@Provides
fun provideBarCodeOptions() : GmsBarcodeScannerOptions {
return GmsBarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
}
@ViewModelScoped
@Provides
fun provideBarCodeScanner(@ApplicationContext context: Context, options: GmsBarcodeScannerOptions) : GmsBarcodeScanner {
return GmsBarcodeScanning.getClient(context, options)
}
@ViewModelScoped
@Provides
fun provideQRScanner(gmsBarcodeScanner: GmsBarcodeScanner) : CodeScanner {
return QRScanner(gmsBarcodeScanner)
}
}
@@ -1,7 +1,6 @@
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.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
@@ -12,7 +11,6 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent
import dagger.hilt.android.scopes.ServiceScoped
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ServiceComponent::class)
@@ -29,4 +27,8 @@ abstract class ServiceModule {
@Binds
@ServiceScoped
abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService<MobileDataService>
@Binds
@ServiceScoped
abstract fun provideEthernetService(ethernetService: EthernetService) : NetworkService<EthernetService>
}
@@ -3,16 +3,11 @@ 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.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.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -21,22 +16,17 @@ import javax.inject.Inject
class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var settingsRepo : Repository<Settings>
lateinit var settingsRepo : SettingsDoa
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
CoroutineScope(SupervisorJob()).launch {
CoroutineScope(Dispatchers.IO).launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
if (settings.isNotEmpty()) {
val setting = settings.first()
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
ServiceTracker.actionOnService(
Action.START, context,
WireGuardConnectivityWatcherService::class.java,
mapOf(context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!)
)
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
}
}
} finally {
@@ -3,15 +3,12 @@ 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.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -21,31 +18,17 @@ import javax.inject.Inject
class NotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var settingsRepo : Repository<Settings>
lateinit var settingsRepo : SettingsDoa
override fun onReceive(context: Context, intent: Intent?) {
CoroutineScope(SupervisorJob()).launch {
CoroutineScope(Dispatchers.IO).launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
if (settings.isNotEmpty()) {
val setting = settings.first()
if (setting.defaultTunnel != null) {
ServiceTracker.actionOnService(
Action.STOP, context,
WireGuardTunnelService::class.java,
mapOf(
context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!
)
)
delay(1000)
ServiceTracker.actionOnService(
Action.START, context,
WireGuardTunnelService::class.java,
mapOf(
context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!
)
)
ServiceManager.stopVpnService(context)
delay(Constants.TOGGLE_TUNNEL_DELAY)
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
}
}
} finally {
@@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.repository
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
@Database(entities = [Settings::class, TunnelConfig::class], version = 1, exportSchema = false)
@TypeConverters(DatabaseListConverters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun settingDao(): SettingsDoa
abstract fun tunnelConfigDoa() : TunnelConfigDao
}
@@ -0,0 +1,15 @@
package com.zaneschepke.wireguardautotunnel.repository
import androidx.room.TypeConverter
class DatabaseListConverters {
@TypeConverter
fun listToString(value: MutableList<String>): String {
return value.joinToString(",")
}
@TypeConverter
fun <T> stringToList(value: String): MutableList<String> {
if(value.isEmpty()) return mutableListOf()
return value.split(",").toMutableList()
}
}
@@ -1,16 +0,0 @@
package com.zaneschepke.wireguardautotunnel.repository
import kotlinx.coroutines.flow.Flow
interface Repository<T> {
suspend fun save(t : T)
suspend fun saveAll(t : List<T>)
suspend fun getById(id : Long) : T?
suspend fun getAll() : List<T>?
suspend fun delete(t : T) : Boolean?
suspend fun count() : Long?
val itemFlow : Flow<MutableList<T>>
fun init()
}
@@ -1,63 +0,0 @@
package com.zaneschepke.wireguardautotunnel.repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.kotlin.awaitCallInTx
import io.objectbox.kotlin.toFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import javax.inject.Inject
class SettingsBox @Inject constructor(private val box : Box<Settings>, private val boxStore : BoxStore) : Repository<Settings> {
@OptIn(ExperimentalCoroutinesApi::class)
override val itemFlow = box.query().build().subscribe().toFlow()
override fun init() {
CoroutineScope(Dispatchers.IO).launch {
if(getAll().isNullOrEmpty()) {
save(Settings())
}
}
}
override suspend fun save(t : Settings) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun saveAll(t : List<Settings>) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun getById(id: Long): Settings? {
return boxStore.awaitCallInTx {
box[id]
}
}
override suspend fun getAll(): List<Settings>? {
return boxStore.awaitCallInTx {
box.all
}
}
override suspend fun delete(t : Settings): Boolean? {
return boxStore.awaitCallInTx {
box.remove(t)
}
}
override suspend fun count() : Long? {
return boxStore.awaitCallInTx {
box.count()
}
}
}
@@ -0,0 +1,34 @@
package com.zaneschepke.wireguardautotunnel.repository
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
import kotlinx.coroutines.flow.Flow
@Dao
interface SettingsDoa {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(t: Settings)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveAll(t: List<Settings>)
@Query("SELECT * FROM settings WHERE id=:id")
suspend fun getById(id: Long): Settings?
@Query("SELECT * FROM settings")
suspend fun getAll(): List<Settings>
@Query("SELECT * FROM settings")
fun getAllFlow(): Flow<MutableList<Settings>>
@Delete
suspend fun delete(t: Settings)
@Query("SELECT COUNT('id') FROM settings")
suspend fun count(): Long
}
@@ -1,57 +0,0 @@
package com.zaneschepke.wireguardautotunnel.repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.kotlin.awaitCallInTx
import io.objectbox.kotlin.toFlow
import kotlinx.coroutines.ExperimentalCoroutinesApi
import timber.log.Timber
import javax.inject.Inject
class TunnelBox @Inject constructor(private val box : Box<TunnelConfig>,private val boxStore : BoxStore) : Repository<TunnelConfig> {
@OptIn(ExperimentalCoroutinesApi::class)
override val itemFlow = box.query().build().subscribe().toFlow()
override fun init() {
}
override suspend fun save(t : TunnelConfig) {
Timber.d("Saving tunnel config")
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun saveAll(t : List<TunnelConfig>) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun getById(id: Long): TunnelConfig? {
return boxStore.awaitCallInTx {
box[id]
}
}
override suspend fun getAll(): List<TunnelConfig>? {
return boxStore.awaitCallInTx {
box.all
}
}
override suspend fun delete(t : TunnelConfig): Boolean? {
return boxStore.awaitCallInTx {
box.remove(t)
}
}
override suspend fun count() : Long? {
return boxStore.awaitCallInTx {
box.count()
}
}
}
@@ -0,0 +1,34 @@
package com.zaneschepke.wireguardautotunnel.repository
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import kotlinx.coroutines.flow.Flow
@Dao
interface TunnelConfigDao{
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(t: TunnelConfig)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveAll(t: List<TunnelConfig>)
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
suspend fun getById(id: Long): TunnelConfig?
@Query("SELECT * FROM TunnelConfig")
suspend fun getAll(): List<TunnelConfig>
@Delete
suspend fun delete(t: TunnelConfig)
@Query("SELECT COUNT('id') FROM TunnelConfig")
suspend fun count(): Long
@Query("SELECT * FROM tunnelconfig")
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
}
@@ -0,0 +1,16 @@
package com.zaneschepke.wireguardautotunnel.repository.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Settings(
@PrimaryKey(autoGenerate = true) val id : Int = 0,
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled : Boolean = false,
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled") var isTunnelOnMobileDataEnabled : Boolean = false,
@ColumnInfo(name = "trusted_network_ssids") var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
@ColumnInfo(name = "default_tunnel") var defaultTunnel : String? = null,
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled : Boolean = false,
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled") var isTunnelOnEthernetEnabled : Boolean = false,
)
@@ -1,25 +1,21 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.model
package com.zaneschepke.wireguardautotunnel.repository.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.wireguard.config.Config
import io.objectbox.annotation.ConflictStrategy
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
import io.objectbox.annotation.Unique
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.InputStream
@Entity
@Entity(indices = [Index(value = ["name"], unique = true)])
@Serializable
data class TunnelConfig(
@Id
var id : Long = 0,
@Unique(onConflict = ConflictStrategy.REPLACE)
var name : String,
var wgQuick : String
) {
@PrimaryKey(autoGenerate = true) val id : Int = 0,
@ColumnInfo(name = "name") var name : String,
@ColumnInfo(name = "wg_quick") var wgQuick : String,
){
override fun toString(): String {
return Json.encodeToString(serializer(), this)
@@ -1,7 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.barcode
import kotlinx.coroutines.flow.Flow
interface CodeScanner {
fun scan() : Flow<String?>
}
@@ -1,23 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScanner
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import timber.log.Timber
import javax.inject.Inject
class QRScanner @Inject constructor(private val gmsBarcodeScanner: GmsBarcodeScanner) : CodeScanner {
override fun scan(): Flow<String?> {
return callbackFlow {
gmsBarcodeScanner.startScan().addOnSuccessListener {
trySend(it.rawValue)
}.addOnFailureListener {
trySend(it.message)
Timber.e(it.message)
}
awaitClose {
}
}
}
}
@@ -2,5 +2,6 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
enum class Action {
START,
START_FOREGROUND,
STOP
}
@@ -4,10 +4,7 @@ import android.app.Service
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import timber.log.Timber
import javax.inject.Inject
open class ForegroundService : Service() {
@@ -25,7 +22,7 @@ open class ForegroundService : Service() {
val action = intent.action
Timber.d("using an intent with action $action")
when (action) {
Action.START.name -> startService(intent.extras)
Action.START.name, Action.START_FOREGROUND.name -> startService(intent.extras)
Action.STOP.name -> stopService(intent.extras)
"android.net.VpnService" -> {
Timber.d("Always-on VPN starting service")
@@ -0,0 +1,109 @@
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.zaneschepke.wireguardautotunnel.R
import timber.log.Timber
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) {
Timber.e(e.message)
}
}
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)
}
}
}
@@ -1,56 +0,0 @@
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,7 +1,6 @@
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
@@ -9,19 +8,22 @@ 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.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus
import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -38,7 +40,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
lateinit var mobileDataService : NetworkService<MobileDataService>
@Inject
lateinit var settingsRepo: Repository<Settings>
lateinit var ethernetService: NetworkService<EthernetService>
@Inject
lateinit var settingsRepo: SettingsDoa
@Inject
lateinit var notificationService : NotificationService
@@ -46,30 +51,37 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
@Inject
lateinit var vpnService : VpnService
private var isWifiConnected = false;
private var isEthernetConnected = false;
private var isMobileDataConnected = false;
private var currentNetworkSSID = "";
private lateinit var watcherJob : Job;
private lateinit var setting : Settings
private lateinit var tunnelId: String
private var connecting = false
private var disconnecting = false
private var isWifiConnected = false
private var isMobileDataConnected = false
private lateinit var tunnelConfig: String
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.tunnelId = tunnelId
this.tunnelConfig = tunnelId
}
// we need this lock so our service gets not affected by Doze Mode
initWakeLock()
cancelWatcherJob()
launchWatcherNotification()
if(this::tunnelId.isInitialized) {
if(this::tunnelConfig.isInitialized) {
startWatcherJob()
} else {
stopService(extras)
@@ -84,7 +96,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
}
cancelWatcherJob()
stopVPN()
stopSelf()
}
@@ -123,19 +134,27 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
private fun startWatcherJob() {
watcherJob = CoroutineScope(SupervisorJob()).launch {
watcherJob = CoroutineScope(Dispatchers.IO).launch {
val settings = settingsRepo.getAll();
if(!settings.isNullOrEmpty()) {
if(settings.isNotEmpty()) {
setting = settings[0]
}
CoroutineScope(watcherJob).launch {
launch {
watchForWifiConnectivityChanges()
}
if(setting.isTunnelOnMobileDataEnabled) {
CoroutineScope(watcherJob).launch {
launch {
watchForMobileDataConnectivityChanges()
}
}
if(setting.isTunnelOnEthernetEnabled) {
launch {
watchForEthernetConnectivityChanges()
}
}
launch {
manageVpn()
}
}
}
@@ -149,23 +168,34 @@ 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")
}
}
}
}
private suspend fun watchForEthernetConnectivityChanges() {
ethernetService.networkStatus.collect {
when (it) {
is NetworkStatus.Available -> {
Timber.d("Gained Ethernet connection")
isEthernetConnected = true
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Ethernet capabilities changed")
isEthernetConnected = true
}
is NetworkStatus.Unavailable -> {
isEthernetConnected = false
Timber.d("Lost Ethernet connection")
}
}
}
}
private suspend fun watchForWifiConnectivityChanges() {
wifiService.networkStatus.collect {
when (it) {
@@ -176,61 +206,40 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Wifi capabilities changed")
isWifiConnected = true
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()
}
}
currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: "";
}
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 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
private suspend fun manageVpn() {
while(true) {
if(isEthernetConnected && setting.isTunnelOnEthernetEnabled && vpnService.getState() == Tunnel.State.DOWN) {
ServiceManager.startVpnService(this, tunnelConfig)
}
if(!isEthernetConnected && setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected
&& vpnService.getState() == Tunnel.State.DOWN) {
ServiceManager.startVpnService(this, tunnelConfig)
} else if(!isEthernetConnected && !setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
vpnService.getState() == Tunnel.State.UP) {
ServiceManager.stopVpnService(this)
} else if(!isEthernetConnected && isWifiConnected &&
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
(vpnService.getState() != Tunnel.State.UP)) {
ServiceManager.startVpnService(this, tunnelConfig)
} else if(!isEthernetConnected && (isWifiConnected &&
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(vpnService.getState() == Tunnel.State.UP)) {
ServiceManager.stopVpnService(this)
}
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
}
}
}
@@ -5,17 +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.repository.SettingsDoa
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 com.zaneschepke.wireguardautotunnel.repository.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
@@ -29,7 +27,7 @@ class WireGuardTunnelService : ForegroundService() {
lateinit var vpnService : VpnService
@Inject
lateinit var settingsRepo: Repository<Settings>
lateinit var settingsRepo: SettingsDoa
@Inject
lateinit var notificationService : NotificationService
@@ -38,11 +36,19 @@ 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(SupervisorJob()).launch {
job = CoroutineScope(Dispatchers.IO).launch {
if(tunnelConfigString != null) {
try {
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
@@ -55,7 +61,7 @@ class WireGuardTunnelService : ForegroundService() {
} else {
Timber.d("Tunnel config null, starting default tunnel")
val settings = settingsRepo.getAll();
if(!settings.isNullOrEmpty()) {
if(settings.isNotEmpty()) {
val setting = settings[0]
if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
@@ -99,7 +105,7 @@ class WireGuardTunnelService : ForegroundService() {
override fun stopService(extras : Bundle?) {
super.stopService(extras)
CoroutineScope(Dispatchers.IO).launch() {
CoroutineScope(Dispatchers.IO).launch {
vpnService.stopTunnel()
}
cancelJob()
@@ -118,6 +124,18 @@ 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,7 +28,6 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
object : ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO
) {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
@@ -45,6 +44,7 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
@@ -67,6 +67,8 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
}
val request = NetworkRequest.Builder()
.addTransportType(networkCapability)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
@@ -77,8 +79,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.Q) || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R)) {
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
@@ -89,14 +91,15 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
companion object {
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities) : String? {
val info : WifiInfo
if(networkCapabilities.transportInfo is WifiInfo) {
info = networkCapabilities.transportInfo as WifiInfo
} else {
return null
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
}
}
return info.ssid
return null
}
}
}
@@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.service.network
import android.content.Context
import android.net.NetworkCapabilities
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class EthernetService @Inject constructor(@ApplicationContext context: Context) :
BaseNetworkService<EthernetService>(context, NetworkCapabilities.TRANSPORT_ETHERNET) {
}
@@ -0,0 +1,52 @@
package com.zaneschepke.wireguardautotunnel.service.shortcut
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
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
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class ShortcutsActivity : AppCompatActivity() {
@Inject
lateinit var settingsRepo : SettingsDoa
private val scope = CoroutineScope(Dispatchers.Main);
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
scope.launch {
val settings = settingsRepo.getAll()
if (settings.isNotEmpty()) {
val setting = settings.first()
if(setting.isAutoTunnelEnabled) {
ServiceManager.toggleWatcherServiceForeground(this@ShortcutsActivity, tunnelConfig)
}
}
}
}
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 {
attemptWatcherServiceToggle(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()
}
}
@@ -0,0 +1,75 @@
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.repository.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?) {
if(tunnelConfig != null) {
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)
}
}
}
}
@@ -0,0 +1,147 @@
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.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
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 : SettingsDoa
@Inject
lateinit var configRepo : TunnelConfigDao
@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.isNotEmpty()) {
val setting = settings.first()
tunnelConfig = if (setting.defaultTunnel != null) {
TunnelConfig.from(setting.defaultTunnel!!);
} else {
val configs = configRepo.getAll();
val config = if(configs.isNotEmpty()) {
configs.first();
} else {
null
}
config
}
}
return tunnelConfig;
}
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
scope.launch {
val settings = settingsRepo.getAll()
if (settings.isNotEmpty()) {
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();
}
}
}
@@ -3,7 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.crypto.Key
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import kotlinx.coroutines.flow.SharedFlow
interface VpnService : Tunnel {
@@ -5,7 +5,8 @@ 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.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -29,6 +30,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
override val tunnelName get() = _tunnelName.asStateFlow()
private val _state = MutableSharedFlow<Tunnel.State>(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
replay = 1)
private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1,
@@ -103,7 +105,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++
neverHadHandshakeCounter += 10
}
return@forEach
}
@@ -114,7 +116,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
}
}
_lastHandshake.emit(handshakeMap)
delay(1000)
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
}
}
}
@@ -1,15 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.tunnel.model
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
@Entity
data class Settings(
@Id
var id : Long = 0,
var isAutoTunnelEnabled : Boolean = false,
var isTunnelOnMobileDataEnabled : Boolean = false,
var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
var defaultTunnel : String? = null,
var isAlwaysOnVpnEnabled : Boolean = false
)
@@ -0,0 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui
import com.journeyapps.barcodescanner.CaptureActivity
class CaptureActivityPortrait : CaptureActivity()
@@ -6,6 +6,7 @@ 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
@@ -22,6 +23,9 @@ 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
@@ -29,6 +33,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.wireguard.android.backend.GoBackend
import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
@@ -39,7 +44,10 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import java.lang.IllegalStateException
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@@ -51,6 +59,8 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberAnimatedNavController()
val focusRequester = remember { FocusRequester() }
WireguardAutoTunnelTheme {
TransparentSystemBars()
@@ -80,7 +90,25 @@ class MainActivity : AppCompatActivity() {
} else requestNotificationPermission()
}
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) },
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState)},
modifier = Modifier.onKeyEvent {
if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {
when (it.nativeKeyEvent.keyCode) {
KeyEvent.KEYCODE_DPAD_UP -> {
try {
focusRequester.requestFocus()
} catch(e : IllegalStateException) {
Timber.e("No D-Pad focus request modifier added to element on screen")
}
false
} else -> {
false
}
}
} else {
false
}
},
bottomBar = if (vpnIntent == null && notificationPermissionState.status.isGranted) {
{ BottomNavBar(navController, Routes.navItems) }
} else {
@@ -104,8 +132,8 @@ class MainActivity : AppCompatActivity() {
val intentSettings =
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intentSettings.data =
Uri.fromParts("package", this.packageName, null)
startActivity(intentSettings);
Uri.fromParts(Constants.URI_PACKAGE_SCHEME, this.packageName, null)
startActivity(intentSettings)
},
message = getString(R.string.notification_permission_required),
getString(R.string.open_settings)
@@ -117,12 +145,12 @@ class MainActivity : AppCompatActivity() {
when (initialState.destination.route) {
Routes.Settings.name, Routes.Support.name ->
slideInHorizontally(
initialOffsetX = { -1000 },
animationSpec = tween(500)
initialOffsetX = { -Constants.SLIDE_IN_TRANSITION_OFFSET },
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
)
else -> {
fadeIn(animationSpec = tween(1000))
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
}
}
}) {
@@ -132,41 +160,50 @@ class MainActivity : AppCompatActivity() {
when (initialState.destination.route) {
Routes.Main.name ->
slideInHorizontally(
initialOffsetX = { 1000 },
animationSpec = tween(500)
initialOffsetX = { Constants.SLIDE_IN_TRANSITION_OFFSET },
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
)
Routes.Support.name -> {
slideInHorizontally(
initialOffsetX = { -1000 },
animationSpec = tween(500)
initialOffsetX = { -Constants.SLIDE_IN_TRANSITION_OFFSET },
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
)
}
else -> {
fadeIn(animationSpec = tween(1000))
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
}
}
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController) }
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController, focusRequester = focusRequester) }
composable(Routes.Support.name, enterTransition = {
when (initialState.destination.route) {
Routes.Settings.name, Routes.Main.name ->
slideInHorizontally(
initialOffsetX = { 1000 },
animationSpec = tween(500)
initialOffsetX = { Constants.SLIDE_IN_ANIMATION_DURATION },
animationSpec = tween(Constants.SLIDE_IN_ANIMATION_DURATION)
)
else -> {
fadeIn(animationSpec = tween(1000))
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
}
}
}) { SupportScreen(padding = padding) }
}) { SupportScreen(padding = padding, focusRequester) }
composable("${Routes.Config.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(1000))
}) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"))}
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
}) {
val id = it.arguments?.getString("id")
if(!id.isNullOrBlank()) {
ConfigScreen(padding = padding, navController = navController, id = id, focusRequester = focusRequester)}
}
composable("${Routes.Detail.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(1000))
}) { DetailScreen(padding = padding, id = it.arguments?.getString("id")) }
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
}) {
val id = it.arguments?.getString("id")
if(!id.isNullOrBlank()) {
DetailScreen(padding = padding, id = id)
}
}
}
}
}
@@ -0,0 +1,80 @@
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)
)
}
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.config
import android.content.pm.PackageManager
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
@@ -45,6 +44,7 @@ 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)
@@ -52,13 +52,13 @@ import kotlinx.coroutines.launch
fun ConfigScreen(
viewModel: ConfigViewModel = hiltViewModel(),
padding: PaddingValues,
focusRequester: FocusRequester,
navController: NavController,
id : String?
id : String
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
val scope = rememberCoroutineScope()
@@ -68,11 +68,12 @@ fun ConfigScreen(
val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle()
val include by viewModel.include.collectAsStateWithLifecycle()
val allApplications by viewModel.allApplications.collectAsStateWithLifecycle()
val sortedPackages = remember(packages) {
packages.sortedBy { viewModel.getPackageLabel(it) }
}
LaunchedEffect(Unit) {
viewModel.getTunnelById(id)
viewModel.emitAllInternetCapablePackages()
viewModel.emitCurrentPackageConfigurations(id)
viewModel.emitScreenData(id)
}
if(tunnel != null) {
@@ -165,7 +166,17 @@ fun ConfigScreen(
}
}
}
items(packages) { pack ->
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) {
SearchBar(viewModel::emitQueriedPackages);
}
}
items(sortedPackages, key = { it.packageName }) { pack ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
@@ -191,8 +202,7 @@ fun ConfigScreen(
)
}
Text(
pack.applicationInfo.loadLabel(context.packageManager)
.toString(), modifier = Modifier.padding(5.dp)
viewModel.getPackageLabel(pack), modifier = Modifier.padding(5.dp)
)
}
Checkbox(
@@ -228,10 +238,5 @@ fun ConfigScreen(
}
}
}
LaunchedEffect(Unit) {
if(context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
focusRequester.requestFocus()
}
}
}
}
@@ -8,19 +8,25 @@ import android.os.Build
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class ConfigViewModel @Inject constructor(private val application : Application,
private val tunnelRepo : Repository<TunnelConfig>,
private val settingsRepo : Repository<Settings>) : ViewModel() {
private val tunnelRepo : TunnelConfigDao,
private val settingsRepo : SettingsDoa) : ViewModel() {
private val _tunnel = MutableStateFlow<TunnelConfig?>(null)
private val _tunnelName = MutableStateFlow("")
@@ -38,24 +44,37 @@ class ConfigViewModel @Inject constructor(private val application : Application,
private val _allApplications = MutableStateFlow(true)
val allApplications get() = _allApplications.asStateFlow()
suspend fun getTunnelById(id : String?) : TunnelConfig? {
return try {
if(id != null) {
val config = tunnelRepo.getById(id.toLong())
if (config != null) {
_tunnel.emit(config)
_tunnelName.emit(config.name)
fun emitScreenData(id : String) {
viewModelScope.launch(Dispatchers.IO) {
val tunnelConfig = getTunnelConfigById(id);
emitTunnelConfig(tunnelConfig);
emitTunnelConfigName(tunnelConfig?.name)
emitQueriedPackages("")
emitCurrentPackageConfigurations(id)
}
}
}
return config
}
return null
private suspend fun getTunnelConfigById(id : String) : TunnelConfig? {
return try {
tunnelRepo.getById(id.toLong())
} catch (e : Exception) {
Timber.e(e.message)
null
}
}
private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
if(tunnelConfig != null) {
_tunnel.emit(tunnelConfig)
}
}
private suspend fun emitTunnelConfigName(name : String?) {
if(name != null) {
_tunnelName.emit(name)
}
}
fun onTunnelNameChange(name : String) {
_tunnelName.value = name
}
@@ -75,31 +94,71 @@ class ConfigViewModel @Inject constructor(private val application : Application,
_checkedPackages.value.remove(packageName)
}
suspend fun emitCurrentPackageConfigurations(id : String?) {
val tunnelConfig = getTunnelById(id)
if(tunnelConfig != null) {
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
val excludedApps = config.`interface`.excludedApplications
val includedApps = config.`interface`.includedApplications
if(excludedApps.isNullOrEmpty() && includedApps.isNullOrEmpty()) {
_allApplications.emit(true)
return
}
if(excludedApps.isEmpty()) {
_include.emit(true)
_checkedPackages.emit(includedApps.toMutableStateList())
} else {
_include.emit(false)
_checkedPackages.emit(excludedApps.toMutableStateList())
}
_allApplications.emit(false)
private suspend fun emitSplitTunnelConfiguration(config : Config) {
val excludedApps = config.`interface`.excludedApplications
val includedApps = config.`interface`.includedApplications
if (excludedApps.isNotEmpty() || includedApps.isNotEmpty()) {
emitTunnelAllApplicationsDisabled()
determineAppInclusionState(excludedApps, includedApps)
} else {
emitTunnelAllApplicationsEnabled()
}
}
suspend fun emitAllInternetCapablePackages() {
_packages.emit(getAllInternetCapablePackages())
private suspend fun determineAppInclusionState(excludedApps : Set<String>, includedApps : Set<String>) {
if (excludedApps.isEmpty()) {
emitIncludedAppsExist()
emitCheckedApps(includedApps)
} else {
emitExcludedAppsExist()
emitCheckedApps(excludedApps)
}
}
private suspend fun emitIncludedAppsExist() {
_include.emit(true)
}
private suspend fun emitExcludedAppsExist() {
_include.emit(false)
}
private suspend fun emitCheckedApps(apps : Set<String>) {
_checkedPackages.emit(apps.toMutableStateList())
}
private suspend fun emitTunnelAllApplicationsEnabled() {
_allApplications.emit(true)
}
private suspend fun emitTunnelAllApplicationsDisabled() {
_allApplications.emit(false)
}
private fun emitCurrentPackageConfigurations(id : String) {
viewModelScope.launch(Dispatchers.IO) {
val tunnelConfig = getTunnelConfigById(id)
if (tunnelConfig != null) {
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
emitSplitTunnelConfiguration(config)
}
}
}
fun emitQueriedPackages(query : String) {
viewModelScope.launch(Dispatchers.IO) {
val packages = getAllInternetCapablePackages().filter {
getPackageLabel(it).lowercase().contains(query.lowercase())
}
_packages.emit(packages)
}
}
fun getPackageLabel(packageInfo : PackageInfo) : String {
return packageInfo.applicationInfo.loadLabel(application.packageManager).toString()
}
private fun getAllInternetCapablePackages() : List<PackageInfo> {
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
}
@@ -108,39 +167,81 @@ class ConfigViewModel @Inject constructor(private val application : Application,
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackagesHoldingPermissions(permissions, PackageManager.PackageInfoFlags.of(0L))
} else {
@Suppress("DEPRECATION")
packageManager.getPackagesHoldingPermissions(permissions, 0)
}
}
suspend fun onSaveAllChanges() {
private fun removeTunnelShortcuts(tunnelConfig: TunnelConfig?) {
if(tunnelConfig != null) {
ShortcutsManager.removeTunnelShortcuts(application, tunnelConfig)
}
}
private fun isAllApplicationsEnabled() : Boolean {
return _allApplications.value
}
private fun isIncludeApplicationsEnabled() : Boolean {
return _include.value
}
private fun updateQuickStringWithSelectedPackages() : String {
var wgQuick = _tunnel.value?.wgQuick
if(wgQuick != null) {
wgQuick = if(_include.value) {
wgQuick = if(isAllApplicationsEnabled()) {
TunnelConfig.clearAllApplicationsFromConfig(wgQuick)
} else if(isIncludeApplicationsEnabled()) {
TunnelConfig.setIncludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
} else {
TunnelConfig.setExcludedApplicationsOnQuick(_checkedPackages.value, wgQuick)
}
if(_allApplications.value) {
wgQuick = TunnelConfig.clearAllApplicationsFromConfig(wgQuick)
}
_tunnel.value?.copy(
name = _tunnelName.value,
wgQuick = wgQuick
)?.let {
tunnelRepo.save(it)
val settings = settingsRepo.getAll()
if(settings != null) {
val setting = settings[0]
if(setting.defaultTunnel != null) {
if(it.id == TunnelConfig.from(setting.defaultTunnel!!).id) {
settingsRepo.save(setting.copy(
defaultTunnel = it.toString()
))
}
}
} else {
throw WgTunnelException("Wg quick string is null")
}
return wgQuick;
}
private suspend fun saveConfig(tunnelConfig: TunnelConfig) {
tunnelRepo.save(tunnelConfig)
}
private suspend fun updateTunnelConfig(tunnelConfig: TunnelConfig?) {
if(tunnelConfig != null) {
saveConfig(tunnelConfig)
addTunnelShortcuts(tunnelConfig)
updateSettingsDefaultTunnel(tunnelConfig)
}
}
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
val settings = settingsRepo.getAll()
if(settings.isNotEmpty()) {
val setting = settings[0]
if(setting.defaultTunnel != null) {
if(tunnelConfig.id == TunnelConfig.from(setting.defaultTunnel!!).id) {
settingsRepo.save(setting.copy(
defaultTunnel = tunnelConfig.toString()
))
}
}
}
}
private fun addTunnelShortcuts(tunnelConfig: TunnelConfig) {
ShortcutsManager.createTunnelShortcuts(application, tunnelConfig)
}
suspend fun onSaveAllChanges() {
try {
removeTunnelShortcuts(_tunnel.value)
val wgQuick = updateQuickStringWithSelectedPackages()
val tunnelConfig = _tunnel.value?.copy(
name = _tunnelName.value,
wgQuick = wgQuick
)
updateTunnelConfig(tunnelConfig)
} catch (e : Exception) {
Timber.e(e.message)
}
}
}
@@ -36,7 +36,7 @@ import java.time.Instant
fun DetailScreen(
viewModel: DetailViewModel = hiltViewModel(),
padding: PaddingValues,
id : String?
id : String
) {
val clipboardManager: ClipboardManager = LocalClipboardManager.current
@@ -47,15 +47,17 @@ fun DetailScreen(
LaunchedEffect(Unit) {
viewModel.getTunnelById(id)
viewModel.emitConfig(id)
}
if(tunnel != null) {
if(null != tunnel) {
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 = if(optionalMtu?.isPresent == true) optionalMtu.get().toString() else stringResource(
id = R.string.none
)
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
@@ -97,7 +99,9 @@ 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 = if(it.endpoint.isPresent) it.endpoint.get().toString() else stringResource(
id = R.string.none
)
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 {
@@ -123,7 +127,7 @@ fun DetailScreen(
val handshakeEpoch = lastHandshake[it.publicKey]
if(handshakeEpoch != null) {
if(handshakeEpoch == 0L) {
Text("Never")
Text(stringResource(id = R.string.never))
} else {
val time = Instant.ofEpochMilli(handshakeEpoch)
Text("${Duration.between(time, Instant.now()).seconds} seconds ago")
@@ -1,45 +1,45 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.detail
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class DetailViewModel @Inject constructor(private val tunnelRepo : Repository<TunnelConfig>, private val vpnService : VpnService
class DetailViewModel @Inject constructor(private val tunnelRepo : TunnelConfigDao, private val vpnService : VpnService
) : ViewModel() {
private val _tunnel = MutableStateFlow<Config?>(null)
val tunnel get() = _tunnel.asStateFlow()
private val _tunnelName = MutableStateFlow<String>("")
private val _tunnelName = MutableStateFlow("")
val tunnelName = _tunnelName.asStateFlow()
val tunnelStats get() = vpnService.statistics
val lastHandshake get() = vpnService.lastHandshake
private var config : TunnelConfig? = null
suspend fun getTunnelById(id : String?) : TunnelConfig? {
private suspend fun getTunnelConfigById(id: String): TunnelConfig? {
return try {
if(id != null) {
config = tunnelRepo.getById(id.toLong())
if (config != null) {
_tunnel.emit(TunnelConfig.configFromQuick(config!!.wgQuick))
_tunnelName.emit(config!!.name)
}
return config
}
return null
} catch (e : Exception) {
tunnelRepo.getById(id.toLong())
} catch (e: Exception) {
Timber.e(e.message)
null
}
}
fun emitConfig(id: String) {
viewModelScope.launch(Dispatchers.IO) {
val tunnelConfig = getTunnelConfigById(id)
if(tunnelConfig != null) {
_tunnel.emit(TunnelConfig.configFromQuick(tunnelConfig.wgQuick))
}
}
}
}
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
@@ -52,7 +52,6 @@ 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.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@@ -71,10 +70,15 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.Constants
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
import com.zaneschepke.wireguardautotunnel.ui.theme.brickRed
@@ -82,7 +86,7 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.mint
import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
viewModel: MainViewModel = hiltViewModel(), padding: PaddingValues,
@@ -92,7 +96,6 @@ fun MainScreen(
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
val isVisible = rememberSaveable { mutableStateOf(true) }
val focusRequester = remember { FocusRequester() }
val scope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState()
@@ -104,6 +107,7 @@ 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 {
@@ -136,13 +140,22 @@ fun MainScreen(
}
val pickFileLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.GetContent()
) { file ->
if (file != null) {
viewModel.onTunnelFileSelected(file)
}
ActivityResultContracts.StartActivityForResult()
) { result ->
result.data?.data?.let { viewModel.onTunnelFileSelected(it) }
}
val scanLauncher = rememberLauncherForActivityResult(
contract = ScanContract(),
onResult = {
try {
viewModel.onTunnelQrResult(it.contents)
} catch (e : Exception) {
viewModel.showSnackBarMessage(context.getString(R.string.qr_result_failed))
}
}
)
Scaffold(
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(onTap = {
@@ -150,7 +163,7 @@ fun MainScreen(
})
},
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
floatingActionButton = {
AnimatedVisibility(
visible = isVisible.value,
enter = slideInVertically(initialOffsetY = { it * 2 }),
@@ -197,7 +210,20 @@ fun MainScreen(
.fillMaxWidth()
.clickable {
showBottomSheet = false
pickFileLauncher.launch("*/*")
val fileSelectionIntent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Constants.FILES_SHOW_ADVANCED, true)
type = Constants.ALLOWED_FILE_TYPES
}
if(!viewModel.isIntentAvailable(fileSelectionIntent)) {
fileSelectionIntent.action = Intent.ACTION_OPEN_DOCUMENT
fileSelectionIntent.setPackage(null)
if (!viewModel.isIntentAvailable(fileSelectionIntent)) {
viewModel.showSnackBarMessage(context.getString(R.string.no_file_app))
return@clickable
}
}
pickFileLauncher.launch(fileSelectionIntent)
}
.padding(10.dp)
) {
@@ -217,7 +243,13 @@ fun MainScreen(
.clickable {
scope.launch {
showBottomSheet = false
viewModel.onTunnelQRSelected()
val scanOptions = ScanOptions()
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
scanOptions.setOrientationLocked(true)
scanOptions.setPrompt(context.getString(R.string.scanning_qr))
scanOptions.setBeepEnabled(false)
scanOptions.captureActivity = CaptureActivityPortrait::class.java
scanLauncher.launch(scanOptions)
}
}
.padding(10.dp)
@@ -242,16 +274,20 @@ fun MainScreen(
.padding(padding)
) {
LazyColumn(modifier = Modifier.fillMaxSize()
.nestedScroll(nestedScrollConnection),) {
items(tunnels.toList()) { tunnel ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection),
) {
items(tunnels, key = { tunnel -> tunnel.id }) {tunnel ->
val focusRequester = remember { FocusRequester() }
RowListItem(leadingIcon = Icons.Rounded.Circle,
leadingIconColor = when (handshakeStatus) {
leadingIconColor = if (tunnelName == tunnel.name) 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) {
@@ -261,18 +297,18 @@ fun MainScreen(
return@RowListItem
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
selectedTunnel = tunnel;
selectedTunnel = tunnel
},
onClick = {
if(!context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)){
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
} else {
focusRequester.requestFocus()
}
},
},
rowButton = {
if (tunnel.id == selectedTunnel?.id) {
Row() {
Row {
IconButton(onClick = {
navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}")
}) {
@@ -288,30 +324,43 @@ fun MainScreen(
}
}
} else {
if(context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)){
Row() {
IconButton(modifier = Modifier.focusRequester(focusRequester),onClick = {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
}) {
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
Row {
IconButton(
modifier = Modifier.focusRequester(focusRequester),
onClick = {
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
}) {
Icon(Icons.Rounded.Info, "Info")
}
IconButton(onClick = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
scope.launch {
viewModel.showSnackBarMessage(context.resources.getString(R.string.turn_off_tunnel))
viewModel.showSnackBarMessage(
context.resources.getString(
R.string.turn_off_tunnel
)
)
} else {
navController.navigate("${Routes.Config.name}/${tunnel.id}")
}
navController.navigate("${Routes.Config.name}/${tunnel.id}")
}
}) {
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
Icon(
Icons.Rounded.Edit,
stringResource(id = R.string.edit)
)
}
IconButton(onClick = {
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
scope.launch {
viewModel.showSnackBarMessage(context.resources.getString(R.string.turn_off_tunnel))
viewModel.showSnackBarMessage(
context.resources.getString(
R.string.turn_off_tunnel
)
)
} else {
viewModel.onDelete(tunnel)
}
viewModel.onDelete(tunnel)
}
}) {
Icon(
Icons.Rounded.Delete,
@@ -1,46 +1,53 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wireguard.config.BadConfigException
import com.wireguard.config.Config
import com.zaneschepke.wireguardautotunnel.Constants
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.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.shortcut.ShortcutsManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.ViewState
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
import com.zaneschepke.wireguardautotunnel.util.WgTunnelException
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.InputStream
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(private val application : Application,
private val tunnelRepo : Repository<TunnelConfig>,
private val settingsRepo : Repository<Settings>,
private val vpnService: VpnService,
private val codeScanner: CodeScanner
private val tunnelRepo : TunnelConfigDao,
private val settingsRepo : SettingsDoa,
private val vpnService: VpnService
) : ViewModel() {
private val _viewState = MutableStateFlow(ViewState())
val viewState get() = _viewState.asStateFlow()
val tunnels get() = tunnelRepo.itemFlow
val tunnels get() = tunnelRepo.getAllFlow()
val state get() = vpnService.state
val handshakeStatus get() = vpnService.handshakeStatus
@@ -48,14 +55,9 @@ class MainViewModel @Inject constructor(private val application : Application,
private val _settings = MutableStateFlow(Settings())
val settings get() = _settings.asStateFlow()
private val defaultConfigName = {
"tunnel${(Math.random() * 100000).toInt()}"
}
init {
viewModelScope.launch {
settingsRepo.itemFlow.collect {
viewModelScope.launch(Dispatchers.IO) {
settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect {
val settings = it.first()
validateWatcherServiceState(settings)
_settings.emit(settings)
@@ -64,25 +66,19 @@ class MainViewModel @Inject constructor(private val application : Application,
}
private fun validateWatcherServiceState(settings: Settings) {
val watcherState = ServiceTracker.getServiceState(application, WireGuardConnectivityWatcherService::class.java)
val watcherState = ServiceManager.getServiceState(application.applicationContext, WireGuardConnectivityWatcherService::class.java)
if(settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) {
startWatcherService(settings.defaultTunnel!!)
ServiceManager.startWatcherService(application.applicationContext, 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) {
ServiceTracker.actionOnService( Action.STOP, application, WireGuardConnectivityWatcherService::class.java)
ServiceManager.stopWatcherService(application.applicationContext)
val settings = settingsRepo.getAll()
if(!settings.isNullOrEmpty()) {
if(settings.isNotEmpty()) {
val setting = settings[0]
setting.defaultTunnel = null
setting.isAutoTunnelEnabled = false
@@ -91,83 +87,185 @@ class MainViewModel @Inject constructor(private val application : Application,
}
}
tunnelRepo.delete(tunnel)
ShortcutsManager.removeTunnelShortcuts(application.applicationContext, tunnel)
}
}
fun onTunnelStart(tunnelConfig : TunnelConfig) = viewModelScope.launch {
ServiceTracker.actionOnService( Action.START, application, WireGuardTunnelService::class.java,
mapOf(application.resources.getString(R.string.tunnel_extras_key) to tunnelConfig.toString()))
fun onTunnelStart(tunnelConfig : TunnelConfig) {
viewModelScope.launch {
stopActiveTunnel()
startTunnel(tunnelConfig)
}
}
private fun startTunnel(tunnelConfig: TunnelConfig) {
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
}
private suspend fun stopActiveTunnel() {
if(ServiceManager.getServiceState(application.applicationContext,
WireGuardTunnelService::class.java, ) == ServiceState.STARTED) {
onTunnelStop()
delay(Constants.TOGGLE_TUNNEL_DELAY)
}
}
fun onTunnelStop() {
ServiceTracker.actionOnService( Action.STOP, application, WireGuardTunnelService::class.java)
ServiceManager.stopVpnService(application.applicationContext)
}
suspend fun onTunnelQRSelected() {
codeScanner.scan().collect {
if(!it.isNullOrEmpty() && it.contains(application.resources.getString(R.string.config_validation))) {
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 {
showSnackBarMessage(application.resources.getString(R.string.barcode_error))
private fun validateConfigString(config : String) {
if(!config.contains(application.getString(R.string.config_validation))) {
throw WgTunnelException(application.getString(R.string.config_validation))
}
}
fun onTunnelQrResult(result : String) {
viewModelScope.launch(Dispatchers.IO) {
try {
validateConfigString(result)
val tunnelConfig = TunnelConfig(name = NumberUtils.generateRandomTunnelName(), wgQuick = result)
addTunnel(tunnelConfig)
} catch (e : WgTunnelException) {
showSnackBarMessage(e.message ?: application.getString(R.string.unknown_error_message))
}
}
}
private fun validateFileExtension(fileName : String) {
val extension = getFileExtensionFromFileName(fileName)
if(extension != Constants.VALID_FILE_EXTENSION) {
throw WgTunnelException(application.getString(R.string.file_extension_message))
}
}
private fun saveTunnelConfigFromStream(stream : InputStream, fileName : String) {
viewModelScope.launch(Dispatchers.IO) {
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
val config = Config.parse(bufferReader)
val tunnelName = getNameFromFileName(fileName)
addTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
stream.close()
}
}
private fun getInputStreamFromUri(uri: Uri): InputStream {
return application.applicationContext.contentResolver.openInputStream(uri)
?: throw WgTunnelException(application.getString(R.string.stream_failed))
}
fun onTunnelFileSelected(uri : Uri) {
try {
val fileName = getFileName(application.applicationContext, uri)
val extension = getFileExtensionFromFileName(fileName)
if(extension != ".conf") {
viewModelScope.launch {
showSnackBarMessage(application.resources.getString(R.string.file_extension_message))
}
return
}
val stream = application.applicationContext.contentResolver.openInputStream(uri)
stream ?: return
val bufferReader = stream.bufferedReader(charset = Charsets.UTF_8)
val config = Config.parse(bufferReader)
val tunnelName = getNameFromFileName(fileName)
viewModelScope.launch {
tunnelRepo.save(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString()))
}
stream.close()
} catch(_: BadConfigException) {
viewModelScope.launch {
showSnackBarMessage(application.applicationContext.getString(R.string.bad_config))
}
validateFileExtension(fileName)
val stream = getInputStreamFromUri(uri)
saveTunnelConfigFromStream(stream, fileName)
} catch (e : Exception) {
showExceptionMessage(e)
}
}
@SuppressLint("Range")
private fun getFileName(context: Context, uri: Uri): String {
if (uri.scheme == "content") {
val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor ?: return defaultConfigName()
private fun showExceptionMessage(e : Exception) {
when(e) {
is BadConfigException -> {
showSnackBarMessage(application.getString(R.string.bad_config))
}
is WgTunnelException -> {
showSnackBarMessage(e.message ?: application.getString(R.string.unknown_error_message))
}
else -> showSnackBarMessage(application.getString(R.string.unknown_error_message))
}
}
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
saveTunnel(tunnelConfig)
createTunnelAppShortcuts(tunnelConfig)
}
private suspend fun saveTunnel(tunnelConfig : TunnelConfig) {
tunnelRepo.save(tunnelConfig)
}
private fun createTunnelAppShortcuts(tunnelConfig: TunnelConfig) {
ShortcutsManager.createTunnelShortcuts(application.applicationContext, tunnelConfig)
}
private fun getFileNameByCursor(context: Context, uri: Uri) : String {
val cursor = context.contentResolver.query(uri, null, null, null, null)
if(cursor != null) {
cursor.use {
if(cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
return getDisplayNameByCursor(it)
}
} else {
throw WgTunnelException("Failed to initialize cursor")
}
return defaultConfigName()
}
suspend fun showSnackBarMessage(message : String) {
_viewState.emit(_viewState.value.copy(
showSnackbarMessage = true,
snackbarMessage = message,
snackbarActionText = "Okay",
onSnackbarActionClick = {
viewModelScope.launch {
dismissSnackBar()
}
private fun getDisplayNameColumnIndex(cursor: Cursor) : Int {
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if(columnIndex == -1) {
throw WgTunnelException("Cursor out of bounds")
}
return columnIndex
}
fun isIntentAvailable(i: Intent?): Boolean {
val packageManager = application.packageManager
val list = packageManager.queryIntentActivities(
i!!,
PackageManager.MATCH_DEFAULT_ONLY
)
// Ignore the Android TV framework app in the list
var size = list.size
for (ri in list) {
// Ignore stub apps
if (Constants.ANDROID_TV_STUBS == ri.activityInfo.packageName) {
size--
}
))
delay(3000)
dismissSnackBar()
}
return size > 0
}
private fun getDisplayNameByCursor(cursor: Cursor) : String {
if(cursor.moveToFirst()) {
val index = getDisplayNameColumnIndex(cursor)
return cursor.getString(index)
} else {
throw WgTunnelException("Cursor failed to move to first")
}
}
private fun validateUriContentScheme(uri : Uri) {
if (uri.scheme != Constants.URI_CONTENT_SCHEME) {
throw WgTunnelException(application.getString(R.string.file_extension_message))
}
}
private fun getFileName(context: Context, uri: Uri): String {
validateUriContentScheme(uri)
return try {
getFileNameByCursor(context, uri)
} catch (_: Exception) {
NumberUtils.generateRandomTunnelName()
}
}
fun showSnackBarMessage(message : String) {
CoroutineScope(Dispatchers.IO).launch {
_viewState.emit(_viewState.value.copy(
showSnackbarMessage = true,
snackbarMessage = message,
snackbarActionText = application.getString(R.string.okay),
onSnackbarActionClick = {
viewModelScope.launch {
dismissSnackBar()
}
}
))
delay(Constants.SNACKBAR_DELAY)
dismissSnackBar()
}
}
private suspend fun dismissSnackBar() {
@@ -2,8 +2,8 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -13,6 +13,7 @@ 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
@@ -67,7 +68,8 @@ 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.service.tunnel.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.Routes
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
import kotlinx.coroutines.launch
@@ -80,12 +82,12 @@ fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(),
padding: PaddingValues,
navController: NavController,
focusRequester: FocusRequester,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val interactionSource = remember { MutableInteractionSource() }
@@ -94,8 +96,6 @@ fun SettingsScreen(
val settings by viewModel.settings.collectAsStateWithLifecycle()
val trustedSSIDs by viewModel.trustedSSIDs.collectAsStateWithLifecycle()
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()
@@ -133,44 +133,44 @@ fun SettingsScreen(
context.startActivity(intentSettings)
}
}
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)
Row(
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val backgroundLocationState =
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
if(!backgroundLocationState.status.isGranted) {
Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier = Modifier
.fillMaxWidth()
.padding(30.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = {
navController.navigate(Routes.Main.name)
}) {
Text(stringResource(id = R.string.no_thanks))
}
Button(modifier = Modifier.focusRequester(focusRequester), onClick = {
openSettings()
}) {
Text(stringResource(id = R.string.turn_on))
}
LaunchedEffect(Unit) {
if(context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
focusRequester.requestFocus()
.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)
Row(
modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
.fillMaxWidth()
.padding(10.dp) else Modifier
.fillMaxWidth()
.padding(30.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = {
navController.navigate(Routes.Main.name)
}) {
Text(stringResource(id = R.string.no_thanks))
}
Button(modifier = Modifier.focusRequester(focusRequester), onClick = {
openSettings()
}) {
Text(stringResource(id = R.string.turn_on))
}
}
}
return
}
return
}
if(!fineLocationState.status.isGranted) {
@@ -192,11 +192,6 @@ fun SettingsScreen(
}) {
Text(stringResource(id = R.string.request))
}
LaunchedEffect(Unit) {
if(context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
focusRequester.requestFocus()
}
}
}
return
@@ -219,7 +214,7 @@ fun SettingsScreen(
}
return
}
if(!isLocationServicesEnabled) {
if(!isLocationServicesEnabled && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
@@ -244,19 +239,21 @@ fun SettingsScreen(
}) {
Text(stringResource(id = R.string.check_again))
}
LaunchedEffect(Unit) {
if(context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
focusRequester.requestFocus()
}
}
}
return
}
val screenPadding = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 5.dp else 15.dp
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier
modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
.fillMaxHeight(.85f)
.fillMaxWidth()
.verticalScroll(scrollState)
.clickable(indication = null, interactionSource = interactionSource) {
focusManager.clearFocus()
}
.padding(padding) else Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.clickable(indication = null, interactionSource = interactionSource) {
@@ -267,7 +264,7 @@ fun SettingsScreen(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(14.dp),
.padding(screenPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
@@ -282,16 +279,11 @@ fun SettingsScreen(
}
}
)
LaunchedEffect(Unit) {
if(context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
focusRequester.requestFocus()
}
}
}
Text(
stringResource(id = R.string.select_tunnel),
textAlign = TextAlign.Center,
modifier = Modifier.padding(15.dp, bottom = 5.dp, top = 5.dp)
modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
)
ExposedDropdownMenuBox(
expanded = expanded,
@@ -339,12 +331,12 @@ fun SettingsScreen(
Text(
stringResource(R.string.trusted_ssid),
textAlign = TextAlign.Center,
modifier = Modifier.padding(15.dp, bottom = 5.dp, top = 5.dp)
modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
)
FlowRow(
modifier = Modifier.padding(15.dp),
modifier = Modifier.padding(screenPadding),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
verticalArrangement = Arrangement.SpaceEvenly
) {
trustedSSIDs.forEach { ssid ->
ClickableIconButton(onIconClick = {
@@ -359,7 +351,7 @@ fun SettingsScreen(
value = currentText,
onValueChange = { currentText = it },
label = { Text(stringResource(R.string.add_trusted_ssid)) },
modifier = Modifier.padding(start = 15.dp, top = 5.dp),
modifier = Modifier.padding(start = screenPadding, top = 5.dp),
maxLines = 1,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
@@ -385,7 +377,7 @@ fun SettingsScreen(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(14.dp),
.padding(screenPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
@@ -403,7 +395,25 @@ fun SettingsScreen(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(14.dp),
.padding(screenPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Tunnel on Ethernet")
Switch(
enabled = !settings.isAutoTunnelEnabled,
checked = settings.isTunnelOnEthernetEnabled,
onCheckedChange = {
scope.launch {
viewModel.onToggleTunnelOnEthernet()
}
}
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(screenPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
@@ -6,37 +6,41 @@ 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.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.repository.SettingsDoa
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.repository.model.Settings
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.ui.ViewState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
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>
private val tunnelRepo : TunnelConfigDao, private val settingsRepo : SettingsDoa
) : ViewModel() {
private val _trustedSSIDs = MutableStateFlow(emptyList<String>())
val trustedSSIDs = _trustedSSIDs.asStateFlow()
private val _settings = MutableStateFlow(Settings())
val settings get() = _settings.asStateFlow()
val tunnels get() = tunnelRepo.itemFlow
val tunnels get() = tunnelRepo.getAllFlow()
private val _viewState = MutableStateFlow(ViewState())
val viewState get() = _viewState.asStateFlow()
init {
checkLocationServicesEnabled()
viewModelScope.launch {
settingsRepo.itemFlow.collect {
viewModelScope.launch(Dispatchers.IO) {
settingsRepo.getAllFlow().filter { it.isNotEmpty() }.collect {
val settings = it.first()
_settings.emit(settings)
_trustedSSIDs.emit(settings.trustedNetworkSSIDs.toList())
@@ -77,32 +81,18 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
return
}
if(_settings.value.isAutoTunnelEnabled) {
actionOnWatcherService(Action.STOP)
ServiceManager.stopWatcherService(application)
} else {
actionOnWatcherService(Action.START)
if(_settings.value.defaultTunnel != null) {
val defaultTunnel = _settings.value.defaultTunnel
ServiceManager.startWatcherService(application, defaultTunnel!!)
}
}
settingsRepo.save(_settings.value.copy(
isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled
))
}
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)
}
}
}
suspend fun showSnackBarMessage(message : String) {
_viewState.emit(_viewState.value.copy(
showSnackbarMessage = true,
@@ -132,6 +122,18 @@ class SettingsViewModel @Inject constructor(private val application : Applicatio
showSnackBarMessage(application.getString(R.string.select_tunnel_message))
}
}
suspend fun onToggleTunnelOnEthernet() {
if(_settings.value.defaultTunnel != null) {
_settings.emit(
_settings.value.copy(isTunnelOnEthernetEnabled = !_settings.value.isTunnelOnEthernetEnabled)
)
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
@@ -37,10 +37,9 @@ import androidx.compose.ui.unit.sp
import com.zaneschepke.wireguardautotunnel.R
@Composable
fun SupportScreen(padding : PaddingValues) {
fun SupportScreen(padding : PaddingValues, focusRequester: FocusRequester) {
val context = LocalContext.current
val focusRequester = remember { FocusRequester() }
fun openWebPage(url: String) {
val webpage: Uri = Uri.parse(url)
@@ -73,11 +72,6 @@ fun SupportScreen(padding : PaddingValues) {
}) {
Icon(imageVector = ImageVector.vectorResource(R.drawable.github), "Github")
}
LaunchedEffect(Unit) {
if(context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
focusRequester.requestFocus()
}
}
}
Spacer(modifier = Modifier.weight(1f))
Text(stringResource(id = R.string.privacy_policy), style = TextStyle(textDecoration = TextDecoration.Underline),
@@ -13,6 +13,10 @@ object NumberUtils {
return bytes.toBigDecimal().divide(BYTES_IN_KB.toBigDecimal())
}
fun generateRandomTunnelName() : String {
return "tunnel${(Math.random() * 100000).toInt()}"
}
fun formatDecimalTwoPlaces(bigDecimal: BigDecimal) : String {
val df = DecimalFormat("#.##")
return df.format(bigDecimal)
@@ -0,0 +1,3 @@
package com.zaneschepke.wireguardautotunnel.util
class WgTunnelException(message: String) : Exception(message)
+5
View File
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z"/>
</vector>
+5
View File
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="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
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12.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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

+15 -2
View File
@@ -38,7 +38,6 @@
<string name="trusted_ssid_empty_description">Enter SSID</string>
<string name="trusted_ssid_value_description">Submit SSID</string>
<string name="config_validation">[Interface]</string>
<string name="invalid_qr">Invalid QR code.</string>
<string name="add_from_file">Add tunnel from files</string>
<string name="open_file">File Open</string>
<string name="add_from_qr">Add tunnel from QR code</string>
@@ -62,7 +61,6 @@
<string name="public_key">Public key</string>
<string name="barcode_downloading">Waiting for the Barcode UI module to be downloaded.</string>
<string name="barcode_downloading_message">Barcode module downloading. Try again.</string>
<string name="barcode_error">Invalid QR code. Try again.</string>
<string name="addresses">Addresses</string>
<string name="dns_servers">DNS servers</string>
<string name="mtu">MTU</string>
@@ -84,4 +82,19 @@
<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>
<string name="db_name">wg-tunnel-db</string>
<string name="scanning_qr">Scanning for QR</string>
<string name="qr_result_failed">QR scan failed</string>
<string name="none">None</string>
<string name="never">Never</string>
<string name="stream_failed">Failed to open file stream.</string>
<string name="unknown_error_message">An unknown error occurred.</string>
<string name="no_file_app">No file app installed.</string>
</resources>
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

+11 -13
View File
@@ -1,20 +1,18 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
val objectBoxVersion by extra("3.5.1")
val hiltVersion by extra("2.47")
val accompanistVersion by extra("0.31.2-alpha")
dependencies {
classpath("io.objectbox:objectbox-gradle-plugin:$objectBoxVersion")
classpath("com.google.gms:google-services:4.3.15")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.7")
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
classpath(libs.google.services)
classpath(libs.firebase.crashlytics.gradle)
}
}
}
plugins {
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
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.hilt.android) apply false
kotlin("plugin.serialization").version(libs.versions.kotlin).apply(false)
alias(libs.plugins.ksp) apply false
}
+8
View File
@@ -0,0 +1,8 @@
plugins {
`kotlin-dsl` // enable the Kotlin-DSL
}
repositories {
google()
mavenCentral()
}
+29
View File
@@ -0,0 +1,29 @@
import org.gradle.api.invocation.Gradle
object BuildHelper {
private fun getCurrentFlavor(gradle : Gradle): String {
val taskRequestsStr = gradle.startParameter.taskRequests.toString()
val pattern: java.util.regex.Pattern = if (taskRequestsStr.contains("assemble")) {
java.util.regex.Pattern.compile("assemble(\\w+)(Release|Debug)")
} else {
java.util.regex.Pattern.compile("bundle(\\w+)(Release|Debug)")
}
val matcher = pattern.matcher(taskRequestsStr)
val flavor = if (matcher.find()) {
matcher.group(1).lowercase()
} else {
print("NO FLAVOR FOUND")
""
}
return flavor
}
fun isGeneralFlavor(gradle : Gradle) : Boolean {
return getCurrentFlavor(gradle) == "general"
}
fun isReleaseBuild(gradle: Gradle) : Boolean {
return (gradle.startParameter.taskNames.size > 0 && gradle.startParameter.taskNames[0].contains(
"Release"))
}
}
@@ -0,0 +1,10 @@
Features
- Add tunnels via .conf file or QR
- Auto connect to VPN based on Wi-Fi SSID
- Split tunneling by application with search
- Always-on VPN support
- Configurable Trusted Network list
- Quick tile and Shortcuts integration
- Optional auto connect on mobile data
- Automatic service restart after reboot
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

@@ -0,0 +1 @@
An alternative VPN client app for WireGuard with additional features
@@ -0,0 +1 @@
WG Tunnel
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

-2
View File
@@ -21,5 +21,3 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
#enable buildconfig values
android.defaults.buildfeatures.buildconfig=true
+92
View File
@@ -0,0 +1,92 @@
[versions]
accompanist = "0.31.2-alpha"
activityCompose = "1.7.2"
androidx-junit = "1.1.5"
appcompat = "1.6.1"
coreKtx = "1.12.0"
espressoCore = "3.5.1"
firebase-crashlytics-gradle = "2.9.9"
google-services = "4.3.15"
hiltAndroid = "2.48"
hiltNavigationCompose = "1.0.0"
junit = "4.13.2"
kotlinx-serialization-json = "1.5.1"
lifecycle-runtime-compose = "2.6.2"
material-icons-extended = "1.5.1"
material3 = "1.1.1"
navigationCompose = "2.7.2"
roomVersion = "2.6.0-beta01"
timber = "5.0.1"
tunnel = "1.0.20230706"
androidGradlePlugin = "8.2.0-beta03"
kotlin="1.9.10"
ksp="1.9.10-1.0.13"
composeBom="2023.09.00"
firebaseBom="32.2.3"
compose="1.5.1"
crashlytics="18.4.1"
analytics="21.3.0"
composeCompiler="1.5.3"
zxingAndroidEmbedded = "4.3.0"
zxingCore = "3.4.1"
[libraries]
# accompanist
accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" }
accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" }
accompanist-navigation-animation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanist" }
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
#room
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }
#compose
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref="composeBom" }
androidx-compose-ui-test = { module="androidx.compose.ui:ui-test-junit4", version.ref="compose" }
androidx-compose-ui-tooling = { module="androidx.compose.ui:ui-tooling", version.ref="compose" }
androidx-compose-manifest = { module="androidx.compose.ui:ui-test-manifest", version.ref="compose" }
androidx-compose-ui-graphics = { module="androidx.compose.ui:ui-graphics", version.ref="compose" }
androidx-compose-ui-tooling-preview = { module="androidx.compose.ui:ui-tooling-preview", version.ref="compose" }
androidx-compose-ui = { module="androidx.compose.ui:ui", version.ref="compose" }
#hilt
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-compose" }
androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
junit = { module = "junit:junit", version.ref = "junit" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle-runtime-compose" }
material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "material-icons-extended" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
tunnel = { module = "com.wireguard.android:tunnel", version.ref = "tunnel" }
#firebase
google-firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx", version.ref = "crashlytics" }
google-firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx", version.ref = "analytics" }
firebase-crashlytics-gradle = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "firebase-crashlytics-gradle" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom"}
google-services = { module = "com.google.gms:google-services", version.ref = "google-services" }
zxing-core = { module = "com.google.zxing:core", version.ref = "zxingCore" }
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+1 -1
View File
@@ -1,6 +1,6 @@
#Mon Apr 24 22:46:45 EDT 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists