From 8d2e8b3d26d9f6295b4b489bdfd07884427acb9e Mon Sep 17 00:00:00 2001 From: pierre Date: Wed, 6 Sep 2023 18:36:01 +0200 Subject: [PATCH] init --- nym-vpn/android/.gitignore | 71 +++ nym-vpn/android/.gitkeep | 0 nym-vpn/android/LICENSE | 21 + nym-vpn/android/README.md | 42 ++ nym-vpn/android/app/.gitignore | 2 + nym-vpn/android/app/build.gradle.kts | 159 +++++++ .../android/app/objectbox-models/default.json | 99 +++++ .../app/objectbox-models/default.json.bak | 94 ++++ nym-vpn/android/app/proguard-rules.pro | 21 + .../ExampleInstrumentedTest.kt | 24 + .../android/app/src/main/AndroidManifest.xml | 115 +++++ .../app/src/main/ic_launcher-playstore.png | Bin 0 -> 39338 bytes .../wireguardautotunnel/Constants.kt | 6 + .../WireGuardAutoTunnel.kt | 27 ++ .../wireguardautotunnel/module/BoxModule.kt | 40 ++ .../module/RepositoryModule.kt | 25 ++ .../module/ServiceModule.kt | 29 ++ .../module/TunnelModule.kt | 31 ++ .../receiver/BootReceiver.kt | 39 ++ .../receiver/NotificationActionReceiver.kt | 39 ++ .../repository/Repository.kt | 16 + .../repository/SettingsBox.kt | 63 +++ .../repository/TunnelBox.kt | 57 +++ .../service/foreground/Action.kt | 6 + .../service/foreground/ForegroundService.kt | 64 +++ .../service/foreground/ServiceManager.kt | 90 ++++ .../service/foreground/ServiceState.kt | 6 + .../WireGuardConnectivityWatcherService.kt | 210 +++++++++ .../foreground/WireGuardTunnelService.kt | 154 +++++++ .../service/network/BaseNetworkService.kt | 117 +++++ .../service/network/MobileDataService.kt | 10 + .../service/network/NetworkService.kt | 10 + .../service/network/NetworkStatus.kt | 10 + .../service/network/WifiService.kt | 10 + .../notification/NotificationService.kt | 21 + .../notification/WireGuardNotification.kt | 77 ++++ .../service/shortcut/ShortcutsActivity.kt | 28 ++ .../service/shortcut/ShortcutsManager.kt | 73 +++ .../service/tile/TunnelControlTile.kt | 142 ++++++ .../service/tunnel/HandshakeStatus.kt | 14 + .../service/tunnel/VpnService.kt | 18 + .../service/tunnel/WireGuardTunnel.kt | 131 ++++++ .../service/tunnel/model/Settings.kt | 15 + .../service/tunnel/model/TunnelConfig.kt | 89 ++++ .../wireguardautotunnel/ui/MainActivity.kt | 201 +++++++++ .../wireguardautotunnel/ui/Routes.kt | 36 ++ .../wireguardautotunnel/ui/ViewState.kt | 9 + .../ui/common/ClickableIconButton.kt | 31 ++ .../common/PermissionRequestFailedScreen.kt | 35 ++ .../ui/common/RowListItem.kt | 55 +++ .../ui/common/SearchBar.kt | 80 ++++ .../ui/common/navigation/BottomNavBar.kt | 42 ++ .../ui/common/navigation/BottomNavItem.kt | 9 + .../ui/screens/config/ConfigScreen.kt | 241 ++++++++++ .../ui/screens/config/ConfigViewModel.kt | 152 +++++++ .../ui/screens/detail/DetailScreen.kt | 139 ++++++ .../ui/screens/detail/DetailViewModel.kt | 45 ++ .../ui/screens/main/MainScreen.kt | 336 ++++++++++++++ .../ui/screens/main/MainViewModel.kt | 179 ++++++++ .../ui/screens/settings/SettingsScreen.kt | 415 ++++++++++++++++++ .../ui/screens/settings/SettingsViewModel.kt | 124 ++++++ .../ui/screens/support/SupportScreen.kt | 83 ++++ .../wireguardautotunnel/ui/theme/Color.kt | 17 + .../wireguardautotunnel/ui/theme/Theme.kt | 80 ++++ .../ui/theme/TransparentSystemBars.kt | 22 + .../wireguardautotunnel/ui/theme/Type.kt | 34 ++ .../wireguardautotunnel/util/NumberUtils.kt | 25 ++ .../app/src/main/res/drawable/discord.xml | 10 + .../app/src/main/res/drawable/github.xml | 12 + .../res/drawable/ic_launcher_background.xml | 170 +++++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++ .../app/src/main/res/drawable/shield.xml | 5 + .../app/src/main/res/drawable/vpn_off.xml | 5 + .../app/src/main/res/drawable/vpn_on.xml | 5 + .../main/res/mipmap-anydpi-v26/ic_banner.xml | 5 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 2004 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 2936 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 3568 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 1268 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 1926 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 2242 bytes .../src/main/res/mipmap-xhdpi/ic_banner.png | Bin 0 -> 3578 bytes .../res/mipmap-xhdpi/ic_banner_foreground.png | Bin 0 -> 3770 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 2728 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 3956 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 5030 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 4246 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 6154 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 7718 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 5854 bytes .../ic_launcher_foreground.webp | Bin 0 -> 8314 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 10402 bytes .../app/src/main/res/values/colors.xml | 11 + .../main/res/values/ic_banner_background.xml | 4 + .../res/values/ic_launcher_background.xml | 4 + .../app/src/main/res/values/strings.xml | 90 ++++ .../app/src/main/res/values/themes.xml | 6 + .../app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../wireguardautotunnel/ExampleUnitTest.kt | 17 + nym-vpn/android/build.gradle.kts | 19 + nym-vpn/android/gradle.properties | 25 ++ .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + nym-vpn/android/gradlew | 185 ++++++++ nym-vpn/android/gradlew.bat | 89 ++++ nym-vpn/android/settings.gradle.kts | 18 + 109 files changed, 5463 insertions(+) create mode 100644 nym-vpn/android/.gitignore delete mode 100644 nym-vpn/android/.gitkeep create mode 100644 nym-vpn/android/LICENSE create mode 100644 nym-vpn/android/README.md create mode 100644 nym-vpn/android/app/.gitignore create mode 100644 nym-vpn/android/app/build.gradle.kts create mode 100644 nym-vpn/android/app/objectbox-models/default.json create mode 100644 nym-vpn/android/app/objectbox-models/default.json.bak create mode 100644 nym-vpn/android/app/proguard-rules.pro create mode 100644 nym-vpn/android/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt create mode 100644 nym-vpn/android/app/src/main/AndroidManifest.xml create mode 100644 nym-vpn/android/app/src/main/ic_launcher-playstore.png create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/BoxModule.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/Repository.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/SettingsBox.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/TunnelBox.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/Action.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceState.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkStatus.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/model/Settings.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/model/TunnelConfig.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ViewState.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavItem.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/TransparentSystemBars.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt create mode 100644 nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt create mode 100644 nym-vpn/android/app/src/main/res/drawable/discord.xml create mode 100644 nym-vpn/android/app/src/main/res/drawable/github.xml create mode 100644 nym-vpn/android/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 nym-vpn/android/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 nym-vpn/android/app/src/main/res/drawable/shield.xml create mode 100644 nym-vpn/android/app/src/main/res/drawable/vpn_off.xml create mode 100644 nym-vpn/android/app/src/main/res/drawable/vpn_on.xml create mode 100644 nym-vpn/android/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml create mode 100644 nym-vpn/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 nym-vpn/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 nym-vpn/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 nym-vpn/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 nym-vpn/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 nym-vpn/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 nym-vpn/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 nym-vpn/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 nym-vpn/android/app/src/main/res/mipmap-xhdpi/ic_banner.png create mode 100644 nym-vpn/android/app/src/main/res/mipmap-xhdpi/ic_banner_foreground.png create mode 100644 nym-vpn/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 nym-vpn/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 nym-vpn/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 nym-vpn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 nym-vpn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 nym-vpn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 nym-vpn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 nym-vpn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 nym-vpn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 nym-vpn/android/app/src/main/res/values/colors.xml create mode 100644 nym-vpn/android/app/src/main/res/values/ic_banner_background.xml create mode 100644 nym-vpn/android/app/src/main/res/values/ic_launcher_background.xml create mode 100644 nym-vpn/android/app/src/main/res/values/strings.xml create mode 100644 nym-vpn/android/app/src/main/res/values/themes.xml create mode 100644 nym-vpn/android/app/src/main/res/xml/backup_rules.xml create mode 100644 nym-vpn/android/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 nym-vpn/android/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt create mode 100644 nym-vpn/android/build.gradle.kts create mode 100644 nym-vpn/android/gradle.properties create mode 100644 nym-vpn/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 nym-vpn/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 nym-vpn/android/gradlew create mode 100644 nym-vpn/android/gradlew.bat create mode 100644 nym-vpn/android/settings.gradle.kts diff --git a/nym-vpn/android/.gitignore b/nym-vpn/android/.gitignore new file mode 100644 index 0000000000..31caa0cedf --- /dev/null +++ b/nym-vpn/android/.gitignore @@ -0,0 +1,71 @@ +# Built application files +*.apk +*.aar +*.ap_ +*.aab +# Files for the ART/Dalvik VM +*.dex +# Java class files +*.class +# Generated files +bin/ +gen/ +out/ +release/ +build/ +# Gradle files +.gradle/ +# Local configuration file (sdk path, etc) +local.properties +# Proguard folder generated by Eclipse +proguard/ +# Log Files +*.log +# Android Studio Navigation editor temp files +.navigation/ +# Android Studio captures folder +captures/ +# IntelliJ +*.iml +.idea/ +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/gradle.xml +# .idea/assetWizardSettings.xml +# .idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +*.jks +*.keystore +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ +# Freeline +freeline.py +freeline/ +freeline_project_description.json +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md +# Version control +vcs.xml +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ +# MacOS +.DS_Store +# App Specific cases +app/release/output.json +.idea/codeStyles/ diff --git a/nym-vpn/android/.gitkeep b/nym-vpn/android/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/nym-vpn/android/LICENSE b/nym-vpn/android/LICENSE new file mode 100644 index 0000000000..51f3844dba --- /dev/null +++ b/nym-vpn/android/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 NymConnect + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/nym-vpn/android/README.md b/nym-vpn/android/README.md new file mode 100644 index 0000000000..e4a334e993 --- /dev/null +++ b/nym-vpn/android/README.md @@ -0,0 +1,42 @@ +## NymConnect for Android + +### Prerequisites + +_TODO_ + +### Getting started + +[Install](https://developer.android.com/studio/install) Android Studio and open +the project.\ +Setup an android emulator using AVD.\ +[Run](https://developer.android.com/studio/run/emulator) the project. + +**⚠ NOTE**: be sure +to [set](https://developer.android.com/studio/run#changing-variant) +the build variant to `x86_64Debug` when running on emulator + +### Features + +* Add tunnels via .conf file +* Auto connect to VPN based on Wi-Fi SSID +* Split tunneling by application with search +* Always-on VPN for Android support +* Quick tile support for vpn toggling +* Dynamic shortcuts support for automation integration +* Configurable Trusted Network list +* Optional auto connect on mobile data +* Automatic service restart after reboot +* Service will stay running in background after app has been closed + +### Building + +_TODO_ + +### Credits + +This project is based on the "WG Tunnel" project made by Zane Schepke +https://github.com/zaneschepke/wgtunnel + +### License + +MIT diff --git a/nym-vpn/android/app/.gitignore b/nym-vpn/android/app/.gitignore new file mode 100644 index 0000000000..956c004dc0 --- /dev/null +++ b/nym-vpn/android/app/.gitignore @@ -0,0 +1,2 @@ +/build +/release \ No newline at end of file diff --git a/nym-vpn/android/app/build.gradle.kts b/nym-vpn/android/app/build.gradle.kts new file mode 100644 index 0000000000..bc158e58e9 --- /dev/null +++ b/nym-vpn/android/app/build.gradle.kts @@ -0,0 +1,159 @@ +val rExtra = rootProject.extra + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + kotlin("kapt") + id("com.google.dagger.hilt.android") + id("org.jetbrains.kotlin.plugin.serialization") + id("io.objectbox") +} + +android { + namespace = "net.nymtech.nymconnect" + compileSdk = 34 + + val versionMajor = 1 + val versionMinor = 0 + val versionPatch = 0 + val versionBuild = 0 + + defaultConfig { + applicationId = "net.nymtech.nymconnect" + minSdk = 28 + targetSdk = 34 + versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild + versionName = "${versionMajor}.${versionMinor}.${versionPatch}" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isDebuggable = false + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.4.8" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + + /* flavorDimensions += "abi" + productFlavors { + create("universal") { + dimension = "abi" + ndk { + abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86") + } + } + create("arch64") { + dimension = "abi" + ndk { + abiFilters += listOf("arm64-v8a", "x86_64") + } + } + create("arm64") { + dimension = "abi" + ndk { + abiFilters += "arm64-v8a" + } + } + create("arm") { + dimension = "abi" + ndk { + abiFilters += "armeabi-v7a" + } + } + create("x86_64") { + dimension = "abi" + ndk { + abiFilters += "x86_64" + } + } + create("x86") { + dimension = "abi" + ndk { + abiFilters += "x86" + } + } + } */ +} + +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") + + 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") + + //wireguard tunnel + implementation("com.wireguard.android:tunnel:1.0.20230706") + + //logging + implementation("com.jakewharton.timber:timber:5.0.1") + + // compose navigation + implementation("androidx.navigation:navigation-compose:2.7.1") + implementation("androidx.hilt:hilt-navigation-compose:1.0.0") + + // hilt + implementation("com.google.dagger:hilt-android:${rExtra.get("hiltVersion")}") + kapt("com.google.dagger:hilt-android-compiler:${rExtra.get("hiltVersion")}") + + //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")}") + + //db + implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}") + + //lifecycle + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1") + + //icons + implementation("androidx.compose.material:material-icons-extended:1.5.0") + + + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") +} + +kapt { + correctErrorTypes = true +} \ No newline at end of file diff --git a/nym-vpn/android/app/objectbox-models/default.json b/nym-vpn/android/app/objectbox-models/default.json new file mode 100644 index 0000000000..dd7a8ab78e --- /dev/null +++ b/nym-vpn/android/app/objectbox-models/default.json @@ -0,0 +1,99 @@ +{ + "_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 +} \ No newline at end of file diff --git a/nym-vpn/android/app/objectbox-models/default.json.bak b/nym-vpn/android/app/objectbox-models/default.json.bak new file mode 100644 index 0000000000..d4084866ef --- /dev/null +++ b/nym-vpn/android/app/objectbox-models/default.json.bak @@ -0,0 +1,94 @@ +{ + "_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 +} \ No newline at end of file diff --git a/nym-vpn/android/app/proguard-rules.pro b/nym-vpn/android/app/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/nym-vpn/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/nym-vpn/android/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt b/nym-vpn/android/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt new file mode 100644 index 0000000000..c006861187 --- /dev/null +++ b/nym-vpn/android/app/src/androidTest/java/com/zaneschepke/wireguardautotunnel/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package net.nymtech.nymconnect + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("net.nymtech.nymconnect", appContext.packageName) + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/AndroidManifest.xml b/nym-vpn/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2ff4f063bb --- /dev/null +++ b/nym-vpn/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/ic_launcher-playstore.png b/nym-vpn/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..e5d8e99564658add3373147cb1504f3eb1e9776f GIT binary patch literal 39338 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 UitnrlZu;iLF&ndYM?E6{2jO2F=>Px# literal 0 HcmV?d00001 diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt new file mode 100644 index 0000000000..c34ccaa493 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt @@ -0,0 +1,6 @@ +package net.nymtech.nymconnect + +object Constants { + const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L; + const val VPN_STATISTIC_CHECK_INTERVAL = 10000L; +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt new file mode 100644 index 0000000000..f0d53e5162 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt @@ -0,0 +1,27 @@ +package net.nymtech.nymconnect + +import android.app.Application +import android.content.Context +import android.content.pm.PackageManager +import net.nymtech.nymconnect.repository.Repository +import net.nymtech.nymconnect.service.tunnel.model.Settings +import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject + +@HiltAndroidApp +class WireGuardAutoTunnel : Application() { + + @Inject + lateinit var settingsRepo : Repository + + override fun onCreate() { + super.onCreate() + settingsRepo.init() + } + + companion object { + fun isRunningOnAndroidTv(context : Context) : Boolean { + return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/BoxModule.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/BoxModule.kt new file mode 100644 index 0000000000..032232369b --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/BoxModule.kt @@ -0,0 +1,40 @@ +package net.nymtech.nymconnect.module + +import android.content.Context +import net.nymtech.nymconnect.service.tunnel.model.MyObjectBox +import net.nymtech.nymconnect.service.tunnel.model.Settings +import net.nymtech.nymconnect.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 { + return store.boxFor(Settings::class.java) + } + + @Provides + @Singleton + fun provideBoxForTunnels(store : BoxStore) : Box { + return store.boxFor(TunnelConfig::class.java) + } + +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt new file mode 100644 index 0000000000..e402249cba --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/RepositoryModule.kt @@ -0,0 +1,25 @@ +package net.nymtech.nymconnect.module + +import net.nymtech.nymconnect.repository.Repository +import net.nymtech.nymconnect.repository.SettingsBox +import net.nymtech.nymconnect.repository.TunnelBox +import net.nymtech.nymconnect.service.tunnel.model.Settings +import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + + @Binds + @Singleton + abstract fun provideSettingsRepository(settingsBox: SettingsBox) : Repository + + @Binds + @Singleton + abstract fun provideTunnelRepository(tunnelBox: TunnelBox) : Repository +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt new file mode 100644 index 0000000000..fa043ec549 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt @@ -0,0 +1,29 @@ +package net.nymtech.nymconnect.module + +import net.nymtech.nymconnect.service.network.MobileDataService +import net.nymtech.nymconnect.service.network.NetworkService +import net.nymtech.nymconnect.service.network.WifiService +import net.nymtech.nymconnect.service.notification.NotificationService +import net.nymtech.nymconnect.service.notification.WireGuardNotification +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ServiceComponent +import dagger.hilt.android.scopes.ServiceScoped + +@Module +@InstallIn(ServiceComponent::class) +abstract class ServiceModule { + + @Binds + @ServiceScoped + abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification) : NotificationService + + @Binds + @ServiceScoped + abstract fun provideWifiService(wifiService: WifiService) : NetworkService + + @Binds + @ServiceScoped + abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt new file mode 100644 index 0000000000..b5962a39d9 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt @@ -0,0 +1,31 @@ +package net.nymtech.nymconnect.module + +import android.content.Context +import com.wireguard.android.backend.Backend +import com.wireguard.android.backend.GoBackend +import net.nymtech.nymconnect.service.tunnel.VpnService +import net.nymtech.nymconnect.service.tunnel.WireGuardTunnel +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 TunnelModule { + + @Provides + @Singleton + fun provideBackend(@ApplicationContext context : Context) : Backend { + return GoBackend(context) + } + + @Provides + @Singleton + fun provideVpnService(backend: Backend) : VpnService { + return WireGuardTunnel(backend) + } + +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt new file mode 100644 index 0000000000..8fcf823c4f --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/BootReceiver.kt @@ -0,0 +1,39 @@ +package net.nymtech.nymconnect.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import net.nymtech.nymconnect.repository.Repository +import net.nymtech.nymconnect.service.foreground.ServiceManager +import net.nymtech.nymconnect.service.tunnel.model.Settings +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class BootReceiver : BroadcastReceiver() { + + @Inject + lateinit var settingsRepo : Repository + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_BOOT_COMPLETED) { + CoroutineScope(Dispatchers.IO).launch { + try { + val settings = settingsRepo.getAll() + if (!settings.isNullOrEmpty()) { + val setting = settings.first() + if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) { + ServiceManager.startWatcherService(context, setting.defaultTunnel!!) + } + } + } finally { + cancel() + } + } + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt new file mode 100644 index 0000000000..97033a8f63 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/receiver/NotificationActionReceiver.kt @@ -0,0 +1,39 @@ +package net.nymtech.nymconnect.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import net.nymtech.nymconnect.repository.Repository +import net.nymtech.nymconnect.service.foreground.ServiceManager +import net.nymtech.nymconnect.service.tunnel.model.Settings +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class NotificationActionReceiver : BroadcastReceiver() { + + @Inject + lateinit var settingsRepo : Repository + override fun onReceive(context: Context, intent: Intent?) { + CoroutineScope(Dispatchers.IO).launch { + try { + val settings = settingsRepo.getAll() + if (!settings.isNullOrEmpty()) { + val setting = settings.first() + if (setting.defaultTunnel != null) { + ServiceManager.stopVpnService(context) + delay(1000) + ServiceManager.startVpnService(context, setting.defaultTunnel.toString()) + } + } + } finally { + cancel() + } + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/Repository.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/Repository.kt new file mode 100644 index 0000000000..187957132c --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/Repository.kt @@ -0,0 +1,16 @@ +package net.nymtech.nymconnect.repository + +import kotlinx.coroutines.flow.Flow + +interface Repository { + suspend fun save(t : T) + suspend fun saveAll(t : List) + suspend fun getById(id : Long) : T? + suspend fun getAll() : List? + suspend fun delete(t : T) : Boolean? + suspend fun count() : Long? + + val itemFlow : Flow> + + fun init() +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/SettingsBox.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/SettingsBox.kt new file mode 100644 index 0000000000..f56247463c --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/SettingsBox.kt @@ -0,0 +1,63 @@ +package net.nymtech.nymconnect.repository + +import net.nymtech.nymconnect.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, private val boxStore : BoxStore) : Repository { + + @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) { + boxStore.awaitCallInTx { + box.put(t) + } + } + + override suspend fun getById(id: Long): Settings? { + return boxStore.awaitCallInTx { + box[id] + } + } + + override suspend fun getAll(): List? { + 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() + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/TunnelBox.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/TunnelBox.kt new file mode 100644 index 0000000000..c57621d53c --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/repository/TunnelBox.kt @@ -0,0 +1,57 @@ +package net.nymtech.nymconnect.repository + +import net.nymtech.nymconnect.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,private val boxStore : BoxStore) : Repository { + + @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) { + boxStore.awaitCallInTx { + box.put(t) + } + } + + override suspend fun getById(id: Long): TunnelConfig? { + return boxStore.awaitCallInTx { + box[id] + } + } + + override suspend fun getAll(): List? { + 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() + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/Action.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/Action.kt new file mode 100644 index 0000000000..ba4f930b12 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/Action.kt @@ -0,0 +1,6 @@ +package net.nymtech.nymconnect.service.foreground + +enum class Action { + START, + STOP +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt new file mode 100644 index 0000000000..9aaf0b93d8 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ForegroundService.kt @@ -0,0 +1,64 @@ +package net.nymtech.nymconnect.service.foreground + +import android.app.Service +import android.content.Intent +import android.os.Bundle +import android.os.IBinder +import timber.log.Timber + + +open class ForegroundService : Service() { + + private var isServiceStarted = false + + override fun onBind(intent: Intent): IBinder? { + // We don't provide binding, so return null + return null + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Timber.d("onStartCommand executed with startId: $startId") + if (intent != null) { + val action = intent.action + Timber.d("using an intent with action $action") + when (action) { + Action.START.name -> startService(intent.extras) + Action.STOP.name -> stopService(intent.extras) + "android.net.VpnService" -> { + Timber.d("Always-on VPN starting service") + startService(intent.extras) + } + else -> Timber.d("This should never happen. No action in the received intent") + } + } else { + Timber.d( + "with a null intent. It has been probably restarted by the system." + ) + } + // by returning this we make sure the service is restarted if the system kills the service + return START_STICKY + } + + + override fun onDestroy() { + super.onDestroy() + Timber.d("The service has been destroyed") + } + + protected open fun startService(extras : Bundle?) { + if (isServiceStarted) return + Timber.d("Starting ${this.javaClass.simpleName}") + isServiceStarted = true + } + + protected open fun stopService(extras : Bundle?) { + Timber.d("Stopping ${this.javaClass.simpleName}") + try { + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + } catch (e: Exception) { + Timber.d("Service stopped without being started: ${e.message}") + } + isServiceStarted = false + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt new file mode 100644 index 0000000000..c00a796453 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt @@ -0,0 +1,90 @@ +package net.nymtech.nymconnect.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 net.nymtech.nymconnect.R +import timber.log.Timber + +object ServiceManager { + @Suppress("DEPRECATION") + private // Deprecated for third party Services. + fun Context.isServiceRunning(service: Class) = + (getSystemService(ACTIVITY_SERVICE) as ActivityManager) + .getRunningServices(Integer.MAX_VALUE) + .any { it.service.className == service.name } + + fun getServiceState(context: Context, cls : Class): ServiceState { + val isServiceRunning = context.isServiceRunning(cls) + return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED + } + + private fun actionOnService(action: Action, context: Context, cls : Class, extras : Map? = 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 -> { + try { + context.startForegroundService(intent) + } catch (e : Exception) { + Timber.e("Unable to start service foreground ${e.message}") + context.startService(intent) + } + } + Action.STOP -> context.startService(intent) + } + } catch (e : Exception) { + Timber.tag("ServiceManager").e(e) + } + } + + 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 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) + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceState.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceState.kt new file mode 100644 index 0000000000..9c26fa0795 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceState.kt @@ -0,0 +1,6 @@ +package net.nymtech.nymconnect.service.foreground + +enum class ServiceState { + STARTED, + STOPPED, +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt new file mode 100644 index 0000000000..fcfa811ea2 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt @@ -0,0 +1,210 @@ +package net.nymtech.nymconnect.service.foreground + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.PowerManager +import android.os.SystemClock +import com.wireguard.android.backend.Tunnel +import net.nymtech.nymconnect.Constants +import net.nymtech.nymconnect.R +import net.nymtech.nymconnect.repository.Repository +import net.nymtech.nymconnect.service.network.MobileDataService +import net.nymtech.nymconnect.service.network.NetworkService +import net.nymtech.nymconnect.service.network.NetworkStatus +import net.nymtech.nymconnect.service.network.WifiService +import net.nymtech.nymconnect.service.notification.NotificationService +import net.nymtech.nymconnect.service.tunnel.VpnService +import net.nymtech.nymconnect.service.tunnel.model.Settings +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class WireGuardConnectivityWatcherService : ForegroundService() { + + private val foregroundId = 122; + + @Inject + lateinit var wifiService : NetworkService + + @Inject + lateinit var mobileDataService : NetworkService + + @Inject + lateinit var settingsRepo: Repository + + @Inject + lateinit var notificationService : NotificationService + + @Inject + lateinit var vpnService : VpnService + + private var isWifiConnected = false; + private var isMobileDataConnected = false; + private var currentNetworkSSID = ""; + + private lateinit var watcherJob : Job; + private lateinit var setting : Settings + private lateinit var tunnelConfig: String + + private var wakeLock: PowerManager.WakeLock? = null + private val tag = this.javaClass.name; + + + override fun startService(extras: Bundle?) { + super.startService(extras) + val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key)) + if (tunnelId != null) { + this.tunnelConfig = tunnelId + } + // we need this lock so our service gets not affected by Doze Mode + initWakeLock() + cancelWatcherJob() + launchWatcherNotification() + if(this::tunnelConfig.isInitialized) { + startWatcherJob() + } else { + stopService(extras) + } + } + + override fun stopService(extras: Bundle?) { + super.stopService(extras) + wakeLock?.let { + if (it.isHeld) { + it.release() + } + } + cancelWatcherJob() + stopSelf() + } + + private fun launchWatcherNotification() { + val notification = notificationService.createNotification( + channelId = getString(R.string.watcher_channel_id), + channelName = getString(R.string.watcher_channel_name), + description = getString(R.string.watcher_notification_text)) + super.startForeground(foregroundId, notification) + } + + //try to start task again if killed + override fun onTaskRemoved(rootIntent: Intent) { + Timber.d("Task Removed called") + val restartServiceIntent = Intent(rootIntent) + val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE); + applicationContext.getSystemService(Context.ALARM_SERVICE); + val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager; + alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent); + } + + private fun initWakeLock() { + wakeLock = + (getSystemService(Context.POWER_SERVICE) as PowerManager).run { + newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply { + acquire() + } + } + } + + private fun cancelWatcherJob() { + if(this::watcherJob.isInitialized) { + watcherJob.cancel() + } + } + + private fun startWatcherJob() { + watcherJob = CoroutineScope(Dispatchers.IO).launch { + val settings = settingsRepo.getAll(); + if(!settings.isNullOrEmpty()) { + setting = settings[0] + } + launch { + watchForWifiConnectivityChanges() + } + if(setting.isTunnelOnMobileDataEnabled) { + launch { + watchForMobileDataConnectivityChanges() + } + } + launch { + manageVpn() + } + } + } + + private suspend fun watchForMobileDataConnectivityChanges() { + mobileDataService.networkStatus.collect { + when(it) { + is NetworkStatus.Available -> { + Timber.d("Gained Mobile data connection") + isMobileDataConnected = true + } + is NetworkStatus.CapabilitiesChanged -> { + isMobileDataConnected = true + Timber.d("Mobile data capabilities changed") + } + is NetworkStatus.Unavailable -> { + isMobileDataConnected = false + Timber.d("Lost mobile data connection") + } + + else -> {} + } + } + } + + private suspend fun watchForWifiConnectivityChanges() { + wifiService.networkStatus.collect { + when (it) { + is NetworkStatus.Available -> { + Timber.d("Gained Wi-Fi connection") + isWifiConnected = true + } + is NetworkStatus.CapabilitiesChanged -> { + Timber.d("Wifi capabilities changed") + isWifiConnected = true + currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: ""; + } + is NetworkStatus.Unavailable -> { + isWifiConnected = false + Timber.d("Lost Wi-Fi connection") + } + + else -> {} + } + } + } + + private suspend fun manageVpn() { + while(watcherJob.isActive) { + if(setting.isTunnelOnMobileDataEnabled && + !isWifiConnected && + isMobileDataConnected + && vpnService.getState() == Tunnel.State.DOWN) { + ServiceManager.startVpnService(this, tunnelConfig) + } else if(!setting.isTunnelOnMobileDataEnabled && + !isWifiConnected && + vpnService.getState() == Tunnel.State.UP) { + ServiceManager.stopVpnService(this) + } else if(isWifiConnected && + !setting.trustedNetworkSSIDs.contains(currentNetworkSSID) && + (vpnService.getState() != Tunnel.State.UP)) { + ServiceManager.startVpnService(this, tunnelConfig) + } else if((isWifiConnected && + setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) && + (vpnService.getState() == Tunnel.State.UP)) { + ServiceManager.stopVpnService(this) + } + delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL) + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt new file mode 100644 index 0000000000..776928e649 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt @@ -0,0 +1,154 @@ +package net.nymtech.nymconnect.service.foreground + +import android.app.PendingIntent +import android.content.Intent +import android.os.Bundle +import net.nymtech.nymconnect.R +import net.nymtech.nymconnect.receiver.NotificationActionReceiver +import net.nymtech.nymconnect.repository.Repository +import net.nymtech.nymconnect.service.notification.NotificationService +import net.nymtech.nymconnect.service.tunnel.HandshakeStatus +import net.nymtech.nymconnect.service.tunnel.VpnService +import net.nymtech.nymconnect.service.tunnel.model.Settings +import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class WireGuardTunnelService : ForegroundService() { + + private val foregroundId = 123; + + @Inject + lateinit var vpnService : VpnService + + @Inject + lateinit var settingsRepo: Repository + + @Inject + lateinit var notificationService : NotificationService + + private lateinit var job : Job + + private var tunnelName : String = "" + + override fun startService(extras : Bundle?) { + super.startService(extras) + val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key)) + cancelJob() + job = CoroutineScope(Dispatchers.IO).launch { + if(tunnelConfigString != null) { + try { + val tunnelConfig = TunnelConfig.from(tunnelConfigString) + tunnelName = tunnelConfig.name + vpnService.startTunnel(tunnelConfig) + launchVpnStartingNotification() + } catch (e : Exception) { + Timber.e("Problem starting tunnel: ${e.message}") + stopService(extras) + } + } else { + Timber.d("Tunnel config null, starting default tunnel") + val settings = settingsRepo.getAll(); + if(!settings.isNullOrEmpty()) { + val setting = settings[0] + if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) { + val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!) + tunnelName = tunnelConfig.name + vpnService.startTunnel(tunnelConfig) + launchVpnStartingNotification() + } + } + } + } + CoroutineScope(job).launch { + var didShowConnected = false + var didShowFailedHandshakeNotification = false + vpnService.handshakeStatus.collect { + when(it) { + HandshakeStatus.NOT_STARTED -> { + } + HandshakeStatus.NEVER_CONNECTED -> { + if(!didShowFailedHandshakeNotification) { + launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message)) + didShowFailedHandshakeNotification = true + didShowConnected = false + } + } + HandshakeStatus.HEALTHY -> { + if(!didShowConnected) { + launchVpnConnectedNotification() + didShowConnected = true + } + } + HandshakeStatus.UNHEALTHY -> { + if(!didShowFailedHandshakeNotification) { + launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message)) + didShowFailedHandshakeNotification = true + didShowConnected = false + } + } + } + } + } + } + + override fun stopService(extras : Bundle?) { + super.stopService(extras) + CoroutineScope(Dispatchers.IO).launch() { + vpnService.stopTunnel() + } + cancelJob() + stopSelf() + } + + private fun launchVpnConnectedNotification() { + val notification = notificationService.createNotification( + channelId = getString(R.string.vpn_channel_id), + channelName = getString(R.string.vpn_channel_name), + title = getString(R.string.tunnel_start_title), + onGoing = false, + showTimestamp = true, + description = "${getString(R.string.tunnel_start_text)} $tunnelName" + ) + 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), + channelName = getString(R.string.vpn_channel_name), + action = PendingIntent.getBroadcast(this,0,Intent(this, NotificationActionReceiver::class.java),PendingIntent.FLAG_IMMUTABLE), + actionText = getString(R.string.restart), + title = getString(R.string.vpn_connection_failed), + onGoing = false, + showTimestamp = true, + description = message + ) + super.startForeground(foregroundId, notification) + } + + + private fun cancelJob() { + if(this::job.isInitialized) { + job.cancel() + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt new file mode 100644 index 0000000000..9574d659ef --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt @@ -0,0 +1,117 @@ +package net.nymtech.nymconnect.service.network + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.net.wifi.SupplicantState +import android.net.wifi.WifiInfo +import android.net.wifi.WifiManager +import android.os.Build +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.map + + +abstract class BaseNetworkService>(val context: Context, networkCapability : Int) : NetworkService { + private val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + private val wifiManager = + context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + + override val networkStatus = callbackFlow { + val networkStatusCallback = when (Build.VERSION.SDK_INT) { + in Build.VERSION_CODES.S..Int.MAX_VALUE -> { + object : ConnectivityManager.NetworkCallback( + FLAG_INCLUDE_LOCATION_INFO + ) { + override fun onAvailable(network: Network) { + trySend(NetworkStatus.Available(network)) + } + + override fun onLost(network: Network) { + trySend(NetworkStatus.Unavailable(network)) + } + + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities)) + } + } + } + + else -> { + object : ConnectivityManager.NetworkCallback() { + + override fun onAvailable(network: Network) { + trySend(NetworkStatus.Available(network)) + } + + override fun onLost(network: Network) { + trySend(NetworkStatus.Unavailable(network)) + } + + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities)) + } + } + } + } + val request = NetworkRequest.Builder() + .addTransportType(networkCapability) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + .build() + connectivityManager.registerNetworkCallback(request, networkStatusCallback) + + awaitClose { + connectivityManager.unregisterNetworkCallback(networkStatusCallback) + } + } + + + override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? { + 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 + } + } + return ssid?.trim('"') + } + + + companion object { + private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val info: WifiInfo + if (networkCapabilities.transportInfo is WifiInfo) { + info = networkCapabilities.transportInfo as WifiInfo + return info.ssid + } + } + return null + } + } +} + +inline fun Flow.map( + crossinline onUnavailable: suspend (network : Network) -> Result, + crossinline onAvailable: suspend (network : Network) -> Result, + crossinline onCapabilitiesChanged: suspend (network : Network, networkCapabilities : NetworkCapabilities) -> Result, +): Flow = map { status -> + when (status) { + is NetworkStatus.Unavailable -> onUnavailable(status.network) + is NetworkStatus.Available -> onAvailable(status.network) + is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(status.network, status.networkCapabilities) + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt new file mode 100644 index 0000000000..a067af06cf --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt @@ -0,0 +1,10 @@ +package net.nymtech.nymconnect.service.network + +import android.content.Context +import android.net.NetworkCapabilities +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class MobileDataService @Inject constructor(@ApplicationContext context: Context) : + BaseNetworkService(context, NetworkCapabilities.TRANSPORT_CELLULAR) { +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt new file mode 100644 index 0000000000..6c274dab03 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt @@ -0,0 +1,10 @@ +package net.nymtech.nymconnect.service.network + +import android.net.NetworkCapabilities +import kotlinx.coroutines.flow.Flow + +interface NetworkService { + fun getNetworkName(networkCapabilities: NetworkCapabilities) : String? + val networkStatus : Flow + +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkStatus.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkStatus.kt new file mode 100644 index 0000000000..5446ce52a6 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkStatus.kt @@ -0,0 +1,10 @@ +package net.nymtech.nymconnect.service.network + +import android.net.Network +import android.net.NetworkCapabilities + +sealed class NetworkStatus { + class Available(val network : Network) : NetworkStatus() + class Unavailable(val network : Network) : NetworkStatus() + class CapabilitiesChanged(val network : Network, val networkCapabilities : NetworkCapabilities) : NetworkStatus() +} diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt new file mode 100644 index 0000000000..a01ebba746 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt @@ -0,0 +1,10 @@ +package net.nymtech.nymconnect.service.network + +import android.content.Context +import android.net.NetworkCapabilities +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class WifiService @Inject constructor(@ApplicationContext context: Context) : + BaseNetworkService(context, NetworkCapabilities.TRANSPORT_WIFI) { +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt new file mode 100644 index 0000000000..4071e64202 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt @@ -0,0 +1,21 @@ +package net.nymtech.nymconnect.service.notification + +import android.app.Notification +import android.app.NotificationManager +import android.app.PendingIntent + +interface NotificationService { + fun createNotification( + channelId: String, + channelName: String, + title: String = "", + action: PendingIntent? = null, + actionText: String? = null, + description: String, + showTimestamp : Boolean = false, + importance: Int = NotificationManager.IMPORTANCE_HIGH, + vibration: Boolean = true, + onGoing: Boolean = true, + lights: Boolean = true + ): Notification +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt new file mode 100644 index 0000000000..4e4cdb3e1b --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/WireGuardNotification.kt @@ -0,0 +1,77 @@ +package net.nymtech.nymconnect.service.notification + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.Color +import net.nymtech.nymconnect.R +import net.nymtech.nymconnect.ui.MainActivity +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : NotificationService { + + private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager; + + override fun createNotification( + channelId: String, + channelName: String, + title: String, + action: PendingIntent?, + actionText: String?, + description: String, + showTimestamp: Boolean, + importance: Int, + vibration: Boolean, + onGoing: Boolean, + lights: Boolean + ): Notification { + val channel = NotificationChannel( + channelId, + channelName, + importance + ).let { + it.description = title + it.enableLights(lights) + it.lightColor = Color.RED + it.enableVibration(vibration) + it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400) + it + } + notificationManager.createNotificationChannel(channel) + val pendingIntent: PendingIntent = + Intent(context, MainActivity::class.java).let { notificationIntent -> + PendingIntent.getActivity( + context, + 0, + notificationIntent, + PendingIntent.FLAG_IMMUTABLE + ) + } + + val builder: Notification.Builder = + Notification.Builder( + context, + channelId + ) + return builder.let { + if(action != null && actionText != null) { + //TODO find a not deprecated way to do this + it.addAction( + Notification.Action.Builder(0, actionText, action) + .build()) + it.setAutoCancel(true) + } + it.setContentTitle(title) + .setContentText(description) + .setContentIntent(pendingIntent) + .setOngoing(onGoing) + .setShowWhen(showTimestamp) + .setSmallIcon(R.mipmap.ic_launcher_foreground) + .build() + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt new file mode 100644 index 0000000000..c88825e71e --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt @@ -0,0 +1,28 @@ +package net.nymtech.nymconnect.service.shortcut + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import net.nymtech.nymconnect.R +import net.nymtech.nymconnect.service.foreground.Action +import net.nymtech.nymconnect.service.foreground.ServiceManager +import net.nymtech.nymconnect.service.foreground.WireGuardTunnelService +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ShortcutsActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if(intent.getStringExtra(ShortcutsManager.CLASS_NAME_EXTRA_KEY) + .equals(WireGuardTunnelService::class.java.name)) { + intent.getStringExtra(getString(R.string.tunnel_extras_key))?.let { + ServiceManager.toggleWatcherService(this, it) + } + when(intent.action){ + Action.STOP.name -> ServiceManager.stopVpnService(this) + Action.START.name -> intent.getStringExtra(getString(R.string.tunnel_extras_key)) + ?.let { ServiceManager.startVpnService(this, it) } + } + } + finish() + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt new file mode 100644 index 0000000000..13df427ebd --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsManager.kt @@ -0,0 +1,73 @@ +package net.nymtech.nymconnect.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 net.nymtech.nymconnect.R +import net.nymtech.nymconnect.service.foreground.Action +import net.nymtech.nymconnect.service.foreground.WireGuardTunnelService +import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig + +object ShortcutsManager { + + private const val SHORT_LABEL_MAX_SIZE = 10; + private const val LONG_LABEL_MAX_SIZE = 25; + private const val APPEND_ON = " On"; + private const val APPEND_OFF = " Off" + const val CLASS_NAME_EXTRA_KEY = "className" + + private fun createAndPushShortcut(context : Context, intent : Intent, id : String, shortLabel : String, + longLabel : String, drawable : Int ) { + val shortcut = ShortcutInfoCompat.Builder(context, id) + .setShortLabel(shortLabel) + .setLongLabel(longLabel) + .setIcon(IconCompat.createWithResource(context, drawable)) + .setIntent(intent) + .build() + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) + } + + fun createTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) { + createAndPushShortcut(context, + createTunnelOnIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())), + tunnelConfig.id.toString() + APPEND_ON, + tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON, + tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON, + R.drawable.vpn_on + ) + createAndPushShortcut(context, + createTunnelOffIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())), + tunnelConfig.id.toString() + APPEND_OFF, + tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF, + tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF, + R.drawable.vpn_off + ) + } + + fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) { + ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON, + tunnelConfig.id.toString() + APPEND_OFF )) + } + + private fun createTunnelOnIntent(context: Context, extras : Map) : 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) : 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) + } + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt new file mode 100644 index 0000000000..38c774c963 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt @@ -0,0 +1,142 @@ +package net.nymtech.nymconnect.service.tile + +import android.os.Build +import android.service.quicksettings.Tile +import android.service.quicksettings.TileService +import com.wireguard.android.backend.Tunnel +import net.nymtech.nymconnect.R +import net.nymtech.nymconnect.repository.Repository +import net.nymtech.nymconnect.service.foreground.ServiceManager +import net.nymtech.nymconnect.service.tunnel.VpnService +import net.nymtech.nymconnect.service.tunnel.model.Settings +import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class TunnelControlTile : TileService() { + + @Inject + lateinit var settingsRepo : Repository + + @Inject + lateinit var configRepo : Repository + + @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.startVpnService(this@TunnelControlTile, tunnel.toString()) + } + } + } catch (e : Exception) { + Timber.e(e.message) + } finally { + cancel() + } + } + } + } + + private suspend fun determineTileTunnel() : TunnelConfig? { + var tunnelConfig : TunnelConfig? = null; + val settings = settingsRepo.getAll() + if (!settings.isNullOrEmpty()) { + val setting = settings.first() + tunnelConfig = if (setting.defaultTunnel != null) { + TunnelConfig.from(setting.defaultTunnel!!); + } else { + val config = configRepo.getAll()?.first(); + config; + } + } + return tunnelConfig; + } + + + private fun attemptWatcherServiceToggle(tunnelConfig : String) { + scope.launch { + val settings = settingsRepo.getAll() + if (!settings.isNullOrEmpty()) { + val setting = settings.first() + if(setting.isAutoTunnelEnabled) { + ServiceManager.toggleWatcherService(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(); + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt new file mode 100644 index 0000000000..1d2fa54ea7 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/HandshakeStatus.kt @@ -0,0 +1,14 @@ +package net.nymtech.nymconnect.service.tunnel + +enum class HandshakeStatus { + HEALTHY, + UNHEALTHY, + NEVER_CONNECTED, + NOT_STARTED; + + companion object { + private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 120 + const val UNHEALTHY_TIME_LIMIT_SEC = WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + 60 + const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30 + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt new file mode 100644 index 0000000000..15c8c1b676 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt @@ -0,0 +1,18 @@ +package net.nymtech.nymconnect.service.tunnel + +import com.wireguard.android.backend.Statistics +import com.wireguard.android.backend.Tunnel +import com.wireguard.crypto.Key +import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig +import kotlinx.coroutines.flow.SharedFlow + +interface VpnService : Tunnel { + suspend fun startTunnel(tunnelConfig : TunnelConfig) : Tunnel.State + suspend fun stopTunnel() + val state : SharedFlow + val tunnelName : SharedFlow + val statistics : SharedFlow + val lastHandshake : SharedFlow> + val handshakeStatus : SharedFlow + fun getState() : Tunnel.State +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt new file mode 100644 index 0000000000..ae9a3c233d --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt @@ -0,0 +1,131 @@ +package net.nymtech.nymconnect.service.tunnel + +import com.wireguard.android.backend.Backend +import com.wireguard.android.backend.BackendException +import com.wireguard.android.backend.Statistics +import com.wireguard.android.backend.Tunnel +import com.wireguard.crypto.Key +import net.nymtech.nymconnect.Constants +import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig +import net.nymtech.nymconnect.util.NumberUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + + +class WireGuardTunnel @Inject constructor(private val backend : Backend, +) : VpnService { + + private val _tunnelName = MutableStateFlow("") + override val tunnelName get() = _tunnelName.asStateFlow() + + private val _state = MutableSharedFlow( + onBufferOverflow = BufferOverflow.DROP_OLDEST, + replay = 1) + + private val _handshakeStatus = MutableSharedFlow(replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST) + override val state get() = _state.asSharedFlow() + + private val _statistics = MutableSharedFlow(replay = 1) + override val statistics get() = _statistics.asSharedFlow() + + private val _lastHandshake = MutableSharedFlow>(replay = 1) + override val lastHandshake get() = _lastHandshake.asSharedFlow() + + override val handshakeStatus: SharedFlow + get() = _handshakeStatus.asSharedFlow() + + private lateinit var statsJob : Job + + + override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{ + return try { + if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) { + stopTunnel() + } + _tunnelName.emit(tunnelConfig.name) + val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) + val state = backend.setState( + this, Tunnel.State.UP, config) + _state.emit(state) + state; + } catch (e : Exception) { + Timber.e("Failed to start tunnel with error: ${e.message}") + Tunnel.State.DOWN + } + } + + override fun getName(): String { + return _tunnelName.value + } + + override suspend fun stopTunnel() { + try { + if(getState() == Tunnel.State.UP) { + val state = backend.setState(this, Tunnel.State.DOWN, null) + _state.emit(state) + } + } catch (e : BackendException) { + Timber.e("Failed to stop tunnel with error: ${e.message}") + } + } + + override fun getState(): Tunnel.State { + return backend.getState(this) + } + + override fun onStateChange(state : Tunnel.State) { + val tunnel = this; + _state.tryEmit(state) + if(state == Tunnel.State.UP) { + statsJob = CoroutineScope(Dispatchers.IO).launch { + val handshakeMap = HashMap() + var neverHadHandshakeCounter = 0 + while (true) { + val statistics = backend.getStatistics(tunnel) + _statistics.emit(statistics) + statistics.peers().forEach { + val handshakeEpoch = statistics.peer(it)?.latestHandshakeEpochMillis ?: 0L + handshakeMap[it] = handshakeEpoch + if(handshakeEpoch == 0L) { + if(neverHadHandshakeCounter >= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) { + _handshakeStatus.emit(HandshakeStatus.NEVER_CONNECTED) + } else { + _handshakeStatus.emit(HandshakeStatus.NOT_STARTED) + } + if(neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) { + neverHadHandshakeCounter += 10 + } + return@forEach + } + if(NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) >= HandshakeStatus.UNHEALTHY_TIME_LIMIT_SEC) { + _handshakeStatus.emit(HandshakeStatus.UNHEALTHY) + } else { + _handshakeStatus.emit(HandshakeStatus.HEALTHY) + } + } + _lastHandshake.emit(handshakeMap) + delay(Constants.VPN_STATISTIC_CHECK_INTERVAL) + } + } + } + if(state == Tunnel.State.DOWN) { + if(this::statsJob.isInitialized) { + statsJob.cancel() + } + _handshakeStatus.tryEmit(HandshakeStatus.NOT_STARTED) + _lastHandshake.tryEmit(emptyMap()) + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/model/Settings.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/model/Settings.kt new file mode 100644 index 0000000000..2aff4e9f11 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/model/Settings.kt @@ -0,0 +1,15 @@ +package net.nymtech.nymconnect.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 = mutableListOf(), + var defaultTunnel : String? = null, + var isAlwaysOnVpnEnabled : Boolean = false, +) diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/model/TunnelConfig.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/model/TunnelConfig.kt new file mode 100644 index 0000000000..91f7440d0d --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/model/TunnelConfig.kt @@ -0,0 +1,89 @@ +package net.nymtech.nymconnect.service.tunnel.model + +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 +@Serializable +data class TunnelConfig( + @Id + var id : Long = 0, + @Unique(onConflict = ConflictStrategy.REPLACE) + var name : String, + var wgQuick : String +) { + + + override fun toString(): String { + return Json.encodeToString(serializer(), this) + } + + companion object { + private const val INCLUDED_APPLICATIONS = "IncludedApplications = " + private const val EXCLUDED_APPLICATIONS = "ExcludedApplications = " + private const val INTERFACE = "[Interface]" + private const val NEWLINE_CHAR = "\n" + private const val APP_CONFIG_SEPARATOR = ", " + + private fun addApplicationsToConfig(appConfig : String, wgQuick : String) : String { + val configList = wgQuick.split(NEWLINE_CHAR).toMutableList() + val interfaceIndex = configList.indexOf(INTERFACE) + configList.add(interfaceIndex + 1, appConfig) + return configList.joinToString(NEWLINE_CHAR) + } + + fun clearAllApplicationsFromConfig(wgQuick : String) : String { + val configList = wgQuick.split(NEWLINE_CHAR).toMutableList() + val itr = configList.iterator() + while (itr.hasNext()) { + val next = itr.next() + if(next.contains(INCLUDED_APPLICATIONS) || next.contains(EXCLUDED_APPLICATIONS)) { + itr.remove() + } + } + return configList.joinToString(NEWLINE_CHAR) + } + + + fun setExcludedApplicationsOnQuick(packages : List, wgQuick: String) : String { + if(packages.isEmpty()) { + return wgQuick + } + val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick) + val excludeConfig = buildExcludedApplicationsString(packages) + return addApplicationsToConfig(excludeConfig, clearedWgQuick) + } + + fun setIncludedApplicationsOnQuick(packages : List, wgQuick: String) : String { + if(packages.isEmpty()) { + return wgQuick + } + val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick) + val includeConfig = buildIncludedApplicationsString(packages) + return addApplicationsToConfig(includeConfig, clearedWgQuick) + } + + private fun buildExcludedApplicationsString(packages : List) : String { + return EXCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR) + } + + private fun buildIncludedApplicationsString(packages : List) : String { + return INCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR) + } + fun from(string : String) : TunnelConfig { + return Json.decodeFromString(string) + } + fun configFromQuick(wgQuick: String): Config { + val inputStream: InputStream = wgQuick.byteInputStream() + val reader = inputStream.bufferedReader(Charsets.UTF_8) + return Config.parse(reader) + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt new file mode 100644 index 0000000000..edb82cc213 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -0,0 +1,201 @@ +package net.nymtech.nymconnect.ui + +import android.Manifest +import android.content.Intent +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 +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.slideInHorizontally +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.LaunchedEffect +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 +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 net.nymtech.nymconnect.R +import net.nymtech.nymconnect.ui.common.PermissionRequestFailedScreen +import net.nymtech.nymconnect.ui.common.navigation.BottomNavBar +import net.nymtech.nymconnect.ui.screens.config.ConfigScreen +import net.nymtech.nymconnect.ui.screens.detail.DetailScreen +import net.nymtech.nymconnect.ui.screens.main.MainScreen +import net.nymtech.nymconnect.ui.screens.settings.SettingsScreen +import net.nymtech.nymconnect.ui.screens.support.SupportScreen +import net.nymtech.nymconnect.ui.theme.TransparentSystemBars +import net.nymtech.nymconnect.ui.theme.WireguardAutoTunnelTheme +import dagger.hilt.android.AndroidEntryPoint +import timber.log.Timber +import java.lang.IllegalStateException + +@AndroidEntryPoint +class MainActivity : AppCompatActivity() { + + @OptIn(ExperimentalAnimationApi::class, + ExperimentalPermissionsApi::class + ) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + val navController = rememberAnimatedNavController() + val focusRequester = remember { FocusRequester() } + + WireguardAutoTunnelTheme { + TransparentSystemBars() + + val snackbarHostState = remember { SnackbarHostState() } + + val notificationPermissionState = + rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) + + fun requestNotificationPermission() { + if (!notificationPermissionState.status.isGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + notificationPermissionState.launchPermissionRequest() + } + } + + var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) } + val vpnActivityResultState = rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult(), + onResult = { + val accepted = (it.resultCode == RESULT_OK) + if (accepted) { + vpnIntent = null + } + }) + LaunchedEffect(vpnIntent) { + if (vpnIntent != null) { + vpnActivityResultState.launch(vpnIntent) + } else requestNotificationPermission() + } + + Scaffold(snackbarHost = { SnackbarHost(snackbarHostState)}, + modifier = Modifier.onKeyEvent { + if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) { + when (it.nativeKeyEvent.keyCode) { + KeyEvent.KEYCODE_DPAD_UP -> { + try { + focusRequester.requestFocus() + } catch(e : IllegalStateException) { + Timber.e("No D-Pad focus request modifier added to element on screen") + } + false + } else -> { + false; + } + } + } else { + false + } + }, + bottomBar = if (vpnIntent == null && notificationPermissionState.status.isGranted) { + { BottomNavBar(navController, Routes.navItems) } + } else { + {} + }, + ) + { padding -> + if (vpnIntent != null) { + PermissionRequestFailedScreen( + padding = padding, + onRequestAgain = { vpnActivityResultState.launch(vpnIntent) }, + message = getString(R.string.vpn_permission_required), + getString(R.string.retry) + ) + return@Scaffold + } + if (!notificationPermissionState.status.isGranted) { + PermissionRequestFailedScreen( + padding = padding, + onRequestAgain = { + val intentSettings = + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + intentSettings.data = + Uri.fromParts("package", this.packageName, null) + startActivity(intentSettings); + }, + message = getString(R.string.notification_permission_required), + getString(R.string.open_settings) + ) + return@Scaffold + } + AnimatedNavHost(navController, startDestination = Routes.Main.name) { + composable(Routes.Main.name, enterTransition = { + when (initialState.destination.route) { + Routes.Settings.name, Routes.Support.name -> + slideInHorizontally( + initialOffsetX = { -1000 }, + animationSpec = tween(500) + ) + + else -> { + fadeIn(animationSpec = tween(1000)) + } + } + }) { + MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController) + } + composable(Routes.Settings.name, enterTransition = { + when (initialState.destination.route) { + Routes.Main.name -> + slideInHorizontally( + initialOffsetX = { 1000 }, + animationSpec = tween(500) + ) + + Routes.Support.name -> { + slideInHorizontally( + initialOffsetX = { -1000 }, + animationSpec = tween(500) + ) + } + + else -> { + fadeIn(animationSpec = tween(1000)) + } + } + }) { 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) + ) + + else -> { + fadeIn(animationSpec = tween(1000)) + } + } + }) { SupportScreen(padding = padding, focusRequester) } + composable("${Routes.Config.name}/{id}", enterTransition = { + fadeIn(animationSpec = tween(1000)) + }) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"), focusRequester = focusRequester)} + composable("${Routes.Detail.name}/{id}", enterTransition = { + fadeIn(animationSpec = tween(1000)) + }) { DetailScreen(padding = padding, id = it.arguments?.getString("id")) } + } + } + } + } + } +} diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt new file mode 100644 index 0000000000..589fa5fbbf --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt @@ -0,0 +1,36 @@ +package net.nymtech.nymconnect.ui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Home +import androidx.compose.material.icons.rounded.QuestionMark +import androidx.compose.material.icons.rounded.Settings +import net.nymtech.nymconnect.ui.common.navigation.BottomNavItem + +enum class Routes { + Main, + Settings, + Support, + Config, + Detail; + + + companion object { + val navItems = listOf( + BottomNavItem( + name = "Tunnels", + route = Main.name, + icon = Icons.Rounded.Home, + ), + BottomNavItem( + name = "Settings", + route = Settings.name, + icon = Icons.Rounded.Settings, + ), + BottomNavItem( + name = "Support", + route = Support.name, + icon = Icons.Rounded.QuestionMark, + ) + ) + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ViewState.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ViewState.kt new file mode 100644 index 0000000000..66aa0b8790 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/ViewState.kt @@ -0,0 +1,9 @@ +package net.nymtech.nymconnect.ui + +data class ViewState( + val showSnackbarMessage : Boolean = false, + val snackbarMessage : String = "", + val snackbarActionText : String = "", + val onSnackbarActionClick : () -> Unit = {}, + val isLoading : Boolean = false +) diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt new file mode 100644 index 0000000000..2ddd1f27b2 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/ClickableIconButton.kt @@ -0,0 +1,31 @@ +package net.nymtech.nymconnect.ui.common + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector + +@Composable +fun ClickableIconButton(onIconClick : () -> Unit, text : String, icon : ImageVector, enabled : Boolean) { + Button(onClick = {}, + enabled = enabled + ) { + Text(text) + Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) + Icon( + imageVector = icon, + contentDescription = "Delete", + modifier = Modifier.size(ButtonDefaults.IconSize).clickable { + if(enabled) { + onIconClick() + } + } + ) + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt new file mode 100644 index 0000000000..a0f81b5745 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/PermissionRequestFailedScreen.kt @@ -0,0 +1,35 @@ +package net.nymtech.nymconnect.ui.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch + +@Composable +fun PermissionRequestFailedScreen(padding : PaddingValues, onRequestAgain : () -> Unit, message : String, buttonText : String ) { + val scope = rememberCoroutineScope() + Column(horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .padding(padding)) { + Text(message, textAlign = TextAlign.Center, modifier = Modifier.padding(15.dp)) + Button(onClick = { + scope.launch { + onRequestAgain() + } + }) { + Text(buttonText) + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt new file mode 100644 index 0000000000..33fb6d0bf6 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt @@ -0,0 +1,55 @@ +package net.nymtech.nymconnect.ui.common + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun RowListItem(leadingIcon : ImageVector? = null, leadingIconColor : Color = Color.Gray, text : String, onHold : () -> Unit, onClick: () -> Unit, rowButton : @Composable() () -> Unit ) { + Box( + modifier = Modifier + .combinedClickable( + onClick = { + onClick() + }, + onLongClick = { + onHold() + } + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row(verticalAlignment = Alignment.CenterVertically,) { + if(leadingIcon != null) { + Icon( + leadingIcon, "status", + tint = leadingIconColor, + modifier = Modifier.padding(end = 10.dp).size(15.dp) + ) + } + Text(text) + } + + rowButton() + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt new file mode 100644 index 0000000000..cec26f5b09 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/SearchBar.kt @@ -0,0 +1,80 @@ +package net.nymtech.nymconnect.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 net.nymtech.nymconnect.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) + ) +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt new file mode 100644 index 0000000000..957228eece --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavBar.kt @@ -0,0 +1,42 @@ +package net.nymtech.nymconnect.ui.common.navigation + +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontWeight +import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState + +@Composable +fun BottomNavBar(navController : NavController, bottomNavItems : List) { + + val backStackEntry = navController.currentBackStackEntryAsState() + + NavigationBar( + containerColor = MaterialTheme.colorScheme.background, + ) { + bottomNavItems.forEach { item -> + val selected = item.route == backStackEntry.value?.destination?.route + + NavigationBarItem( + selected = selected, + onClick = { navController.navigate(item.route) }, + label = { + Text( + text = item.name, + fontWeight = FontWeight.SemiBold, + ) + }, + icon = { + Icon( + imageVector = item.icon, + contentDescription = "${item.name} Icon", + ) + } + ) + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavItem.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavItem.kt new file mode 100644 index 0000000000..a4a585ce5e --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/BottomNavItem.kt @@ -0,0 +1,9 @@ +package net.nymtech.nymconnect.ui.common.navigation + +import androidx.compose.ui.graphics.vector.ImageVector + +data class BottomNavItem( + val name: String, + val route: String, + val icon: ImageVector, +) diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt new file mode 100644 index 0000000000..1ecfaf6570 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -0,0 +1,241 @@ +package net.nymtech.nymconnect.ui.screens.config + +import android.widget.Toast +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Android +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import com.google.accompanist.drawablepainter.DrawablePainter +import net.nymtech.nymconnect.R +import net.nymtech.nymconnect.ui.Routes +import net.nymtech.nymconnect.ui.common.SearchBar +import kotlinx.coroutines.launch + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun ConfigScreen( + viewModel: ConfigViewModel = hiltViewModel(), + padding: PaddingValues, + focusRequester: FocusRequester, + navController: NavController, + id : String? +) { + + val context = LocalContext.current + val focusManager = LocalFocusManager.current + + val keyboardController = LocalSoftwareKeyboardController.current + val scope = rememberCoroutineScope() + val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null) + val tunnelName = viewModel.tunnelName.collectAsStateWithLifecycle() + val packages by viewModel.packages.collectAsStateWithLifecycle() + val checkedPackages by viewModel.checkedPackages.collectAsStateWithLifecycle() + val include by viewModel.include.collectAsStateWithLifecycle() + val allApplications by viewModel.allApplications.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.getTunnelById(id) + viewModel.emitQueriedPackages("") + viewModel.emitCurrentPackageConfigurations(id) + } + + if(tunnel != null) { + LazyColumn( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + OutlinedTextField( + modifier = Modifier.focusRequester(focusRequester), + value = tunnelName.value, + onValueChange = { + viewModel.onTunnelNameChange(it) + }, + label = { Text(stringResource(id = R.string.tunnel_name)) }, + maxLines = 1, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + keyboardController?.hide() + viewModel.onTunnelNameChange(tunnelName.value) + } + ), + ) + } + } + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(id = R.string.tunnel_all)) + Switch( + checked = allApplications, + onCheckedChange = { + viewModel.onAllApplicationsChange(!allApplications) + } + ) + } + } + if (!allApplications) { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(id = R.string.include)) + Checkbox( + checked = include, + onCheckedChange = { + viewModel.onIncludeChange(!include) + } + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(id = R.string.exclude)) + Checkbox( + checked = !include, + onCheckedChange = { + viewModel.onIncludeChange(!include) + } + ) + } + } + } + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween) { + SearchBar(viewModel::emitQueriedPackages); + } + } + items(packages) { pack -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(5.dp) + ) { + val drawable = + pack.applicationInfo?.loadIcon(context.packageManager) + if (drawable != null) { + Image( + painter = DrawablePainter(drawable), + stringResource(id = R.string.icon), + modifier = Modifier.size(50.dp, 50.dp) + ) + } else { + Icon( + Icons.Rounded.Android, + stringResource(id = R.string.edit), + modifier = Modifier.size(50.dp, 50.dp) + ) + } + Text( + pack.applicationInfo.loadLabel(context.packageManager) + .toString(), modifier = Modifier.padding(5.dp) + ) + } + Checkbox( + checked = (checkedPackages.contains(pack.packageName)), + onCheckedChange = { + if (it) viewModel.onAddCheckedPackage(pack.packageName) else viewModel.onRemoveCheckedPackage( + pack.packageName + ) + } + ) + } + } + } + item { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Button(onClick = { + scope.launch { + viewModel.onSaveAllChanges() + Toast.makeText( + context, + context.resources.getString(R.string.config_changes_saved), + Toast.LENGTH_LONG + ).show() + navController.navigate(Routes.Main.name) + } + }, Modifier.padding(25.dp)) { + Text(stringResource(id = R.string.save_changes)) + } + } + } + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt new file mode 100644 index 0000000000..325e5a49bd --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt @@ -0,0 +1,152 @@ +package net.nymtech.nymconnect.ui.screens.config + +import android.Manifest +import android.app.Application +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.toMutableStateList +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import net.nymtech.nymconnect.repository.Repository +import net.nymtech.nymconnect.service.tunnel.model.Settings +import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class ConfigViewModel @Inject constructor(private val application : Application, + private val tunnelRepo : Repository, + private val settingsRepo : Repository) : ViewModel() { + + private val _tunnel = MutableStateFlow(null) + private val _tunnelName = MutableStateFlow("") + val tunnelName get() = _tunnelName.asStateFlow() + val tunnel get() = _tunnel.asStateFlow() + private val _packages = MutableStateFlow(emptyList()) + val packages get() = _packages.asStateFlow() + private val packageManager = application.packageManager + + private val _checkedPackages = MutableStateFlow(mutableStateListOf()) + val checkedPackages get() = _checkedPackages.asStateFlow() + private val _include = MutableStateFlow(true) + val include get() = _include.asStateFlow() + + 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) + + } + return config + } + return null + } catch (e : Exception) { + Timber.e(e.message) + null + } + } + + fun onTunnelNameChange(name : String) { + _tunnelName.value = name + } + + fun onIncludeChange(include : Boolean) { + _include.value = include + } + fun onAddCheckedPackage(packageName : String) { + _checkedPackages.value.add(packageName) + } + + fun onAllApplicationsChange(allApplications : Boolean) { + _allApplications.value = allApplications + } + + fun onRemoveCheckedPackage(packageName : String) { + _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) + } + } + + fun emitQueriedPackages(query : String) { + viewModelScope.launch { + _packages.emit(getAllInternetCapablePackages().filter { + it.packageName.contains(query) + }) + } + } + + private fun getAllInternetCapablePackages() : List { + return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET)) + } + + private fun getPackagesHoldingPermissions(permissions: Array): List { + 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() { + var wgQuick = _tunnel.value?.wgQuick + if(wgQuick != null) { + wgQuick = if(_include.value) { + 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() + )) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt new file mode 100644 index 0000000000..bdf77a5e56 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt @@ -0,0 +1,139 @@ +package net.nymtech.nymconnect.ui.screens.detail + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ClipboardManager +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import net.nymtech.nymconnect.R +import net.nymtech.nymconnect.util.NumberUtils +import java.time.Duration +import java.time.Instant + +@Composable +fun DetailScreen( + viewModel: DetailViewModel = hiltViewModel(), + padding: PaddingValues, + id : String? +) { + + val clipboardManager: ClipboardManager = LocalClipboardManager.current + val tunnelStats by viewModel.tunnelStats.collectAsStateWithLifecycle(null) + val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null) + val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle() + val lastHandshake by viewModel.lastHandshake.collectAsStateWithLifecycle(emptyMap()) + + + LaunchedEffect(Unit) { + viewModel.getTunnelById(id) + } + + if(tunnel != null) { + 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" + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(padding) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column { + Text(stringResource(R.string.config_interface), fontWeight = FontWeight.Bold, fontSize = 20.sp) + Text(stringResource(R.string.name), fontStyle = FontStyle.Italic) + Text(text = tunnelName, modifier = Modifier.clickable { + clipboardManager.setText(AnnotatedString(tunnelName)) + }) + Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic) + Text(text = interfaceKey, modifier = Modifier.clickable { + clipboardManager.setText(AnnotatedString(interfaceKey)) + }) + Text(stringResource(R.string.addresses), fontStyle = FontStyle.Italic) + Text(text = addresses, modifier = Modifier.clickable { + clipboardManager.setText(AnnotatedString(addresses)) + }) + Text(stringResource(R.string.dns_servers), fontStyle = FontStyle.Italic) + Text(text = dnsServers, modifier = Modifier.clickable { + clipboardManager.setText(AnnotatedString(dnsServers)) + }) + Text(stringResource(R.string.mtu), fontStyle = FontStyle.Italic) + Text(text = mtu, modifier = Modifier.clickable { + clipboardManager.setText(AnnotatedString(mtu)) + }) + Box(modifier = Modifier.padding(10.dp)) + 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" + 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 { + clipboardManager.setText(AnnotatedString(peerKey)) + }) + Text(stringResource(id = R.string.allowed_ips), fontStyle = FontStyle.Italic) + Text(text = allowedIps, modifier = Modifier.clickable { + clipboardManager.setText(AnnotatedString(allowedIps)) + }) + Text(stringResource(R.string.endpoint), fontStyle = FontStyle.Italic) + Text(text = endpoint, modifier = Modifier.clickable { + clipboardManager.setText(AnnotatedString(endpoint)) + }) + if (tunnelStats != null) { + val totalRx = tunnelStats?.totalRx() ?: 0 + val totalTx = tunnelStats?.totalTx() ?: 0 + if((totalRx + totalTx != 0L)) { + val rxKB = NumberUtils.bytesToKB(tunnelStats!!.totalRx()) + val txKB = NumberUtils.bytesToKB(tunnelStats!!.totalTx()) + Text(stringResource(R.string.transfer), fontStyle = FontStyle.Italic) + Text("rx: ${NumberUtils.formatDecimalTwoPlaces(rxKB)} KB tx: ${NumberUtils.formatDecimalTwoPlaces(txKB)} KB") + Text(stringResource(R.string.last_handshake), fontStyle = FontStyle.Italic) + val handshakeEpoch = lastHandshake[it.publicKey] + if(handshakeEpoch != null) { + if(handshakeEpoch == 0L) { + Text("Never") + } else { + val time = Instant.ofEpochMilli(handshakeEpoch) + Text("${Duration.between(time, Instant.now()).seconds} seconds ago") + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt new file mode 100644 index 0000000000..cf83878a0b --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt @@ -0,0 +1,45 @@ +package net.nymtech.nymconnect.ui.screens.detail + +import androidx.lifecycle.ViewModel +import com.wireguard.config.Config +import net.nymtech.nymconnect.repository.Repository +import net.nymtech.nymconnect.service.tunnel.VpnService +import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class DetailViewModel @Inject constructor(private val tunnelRepo : Repository, private val vpnService : VpnService + +) : ViewModel() { + + private val _tunnel = MutableStateFlow(null) + val tunnel get() = _tunnel.asStateFlow() + + 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? { + 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) { + Timber.e(e.message) + null + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt new file mode 100644 index 0000000000..e2abdd07b7 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -0,0 +1,336 @@ +package net.nymtech.nymconnect.ui.screens.main + +import android.annotation.SuppressLint +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FileOpen +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.Circle +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.Edit +import androidx.compose.material.icons.rounded.Info +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import com.wireguard.android.backend.Tunnel +import net.nymtech.nymconnect.R +import net.nymtech.nymconnect.WireGuardAutoTunnel +import net.nymtech.nymconnect.service.tunnel.HandshakeStatus +import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig +import net.nymtech.nymconnect.ui.Routes +import net.nymtech.nymconnect.ui.common.RowListItem +import net.nymtech.nymconnect.ui.theme.brickRed +import net.nymtech.nymconnect.ui.theme.mint +import kotlinx.coroutines.launch + +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainScreen( + viewModel: MainViewModel = hiltViewModel(), padding: PaddingValues, + snackbarHostState: SnackbarHostState, navController: NavController +) { + + val haptic = LocalHapticFeedback.current + val context = LocalContext.current + val isVisible = rememberSaveable { mutableStateOf(true) } + val scope = rememberCoroutineScope() + + val sheetState = rememberModalBottomSheetState() + var showBottomSheet by remember { mutableStateOf(false) } + val tunnels by viewModel.tunnels.collectAsStateWithLifecycle(mutableListOf()) + val handshakeStatus by viewModel.handshakeStatus.collectAsStateWithLifecycle(HandshakeStatus.NOT_STARTED) + val viewState = viewModel.viewState.collectAsStateWithLifecycle() + var selectedTunnel by remember { mutableStateOf(null) } + val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN) + val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("") + + // Nested scroll for control FAB + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + // Hide FAB + if (available.y < -1) { + isVisible.value = false + } + // Show FAB + if (available.y > 1) { + isVisible.value = true + } + return Offset.Zero + } + } + } + + LaunchedEffect(viewState.value) { + if (viewState.value.showSnackbarMessage) { + val result = snackbarHostState.showSnackbar( + message = viewState.value.snackbarMessage, + actionLabel = viewState.value.snackbarActionText, + duration = SnackbarDuration.Long, + ) + when (result) { + SnackbarResult.ActionPerformed -> viewState.value.onSnackbarActionClick + SnackbarResult.Dismissed -> viewState.value.onSnackbarActionClick + } + } + } + + val pickFileLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.GetContent() + ) { file -> + if (file != null) { + viewModel.onTunnelFileSelected(file) + } + } + + Scaffold( + modifier = Modifier.pointerInput(Unit) { + detectTapGestures(onTap = { + selectedTunnel = null + }) + }, + floatingActionButtonPosition = FabPosition.End, + floatingActionButton = { + AnimatedVisibility( + visible = isVisible.value, + enter = slideInVertically(initialOffsetY = { it * 2 }), + exit = slideOutVertically(targetOffsetY = { it * 2 }), + ) { + FloatingActionButton( + modifier = Modifier.padding(bottom = 90.dp), + onClick = { + showBottomSheet = true + }, + containerColor = MaterialTheme.colorScheme.secondary, + shape = RoundedCornerShape(16.dp), + ) { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = stringResource(id = R.string.add_tunnel), + tint = Color.DarkGray, + ) + } + } + } + ) { + if (tunnels.isEmpty()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic) + } + } + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { + showBottomSheet = false + }, + sheetState = sheetState + ) { + // Sheet content + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + showBottomSheet = false + pickFileLauncher.launch("*/*") + } + .padding(10.dp) + ) { + Icon( + Icons.Filled.FileOpen, + contentDescription = stringResource(id = R.string.open_file), + modifier = Modifier.padding(10.dp) + ) + Text( + stringResource(id = R.string.add_from_file), + modifier = Modifier.padding(10.dp) + ) + } + Divider() + } + } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + + LazyColumn( + modifier = Modifier.fillMaxSize() + .nestedScroll(nestedScrollConnection), + ) { + itemsIndexed(tunnels.toList()) { index, tunnel -> + val focusRequester = FocusRequester(); + RowListItem(leadingIcon = Icons.Rounded.Circle, + leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) { + HandshakeStatus.HEALTHY -> mint + 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) { + scope.launch { + viewModel.showSnackBarMessage(context.resources.getString(R.string.turn_off_tunnel)) + } + return@RowListItem + } + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + selectedTunnel = tunnel; + }, + onClick = { + if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + navController.navigate("${Routes.Detail.name}/${tunnel.id}") + } else { + focusRequester.requestFocus() + } + }, + rowButton = { + if (tunnel.id == selectedTunnel?.id) { + Row { + IconButton(onClick = { + navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}") + }) { + Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit)) + } + IconButton( + modifier = Modifier.focusable(), + onClick = { viewModel.onDelete(tunnel) }) { + Icon( + Icons.Rounded.Delete, + stringResource(id = R.string.delete) + ) + } + } + } else { + if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) { + Row { + IconButton( + modifier = Modifier.focusRequester(focusRequester), + onClick = { + navController.navigate("${Routes.Detail.name}/${tunnel.id}") + }) { + Icon(Icons.Rounded.Info, "Info") + } + IconButton(onClick = { + if (state == Tunnel.State.UP && tunnel.name == tunnelName) + scope.launch { + viewModel.showSnackBarMessage( + context.resources.getString( + R.string.turn_off_tunnel + ) + ) + } else { + navController.navigate("${Routes.Config.name}/${tunnel.id}") + } + }) { + Icon( + Icons.Rounded.Edit, + stringResource(id = R.string.edit) + ) + } + IconButton(onClick = { + if (state == Tunnel.State.UP && tunnel.name == tunnelName) + scope.launch { + viewModel.showSnackBarMessage( + context.resources.getString( + R.string.turn_off_tunnel + ) + ) + } else { + viewModel.onDelete(tunnel) + } + }) { + Icon( + Icons.Rounded.Delete, + stringResource(id = R.string.delete) + ) + } + Switch( + checked = (state == Tunnel.State.UP && tunnel.name == tunnelName), + onCheckedChange = { checked -> + if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop() + } + ) + } + } else { + Switch( + checked = (state == Tunnel.State.UP && tunnel.name == tunnelName), + onCheckedChange = { checked -> + if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop() + } + ) + } + } + }) + } + } + } + } +} diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt new file mode 100644 index 0000000000..a88b5adcb9 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -0,0 +1,179 @@ +package net.nymtech.nymconnect.ui.screens.main + +import android.annotation.SuppressLint +import android.app.Application +import android.content.Context +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 net.nymtech.nymconnect.R +import net.nymtech.nymconnect.repository.Repository +import net.nymtech.nymconnect.service.foreground.ServiceState +import net.nymtech.nymconnect.service.foreground.ServiceManager +import net.nymtech.nymconnect.service.foreground.WireGuardConnectivityWatcherService +import net.nymtech.nymconnect.service.shortcut.ShortcutsManager +import net.nymtech.nymconnect.service.tunnel.VpnService +import net.nymtech.nymconnect.service.tunnel.model.Settings +import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig +import net.nymtech.nymconnect.ui.ViewState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + + +@HiltViewModel +class MainViewModel @Inject constructor(private val application : Application, + private val tunnelRepo : Repository, + private val settingsRepo : Repository, + private val vpnService: VpnService, +) : ViewModel() { + + private val _viewState = MutableStateFlow(ViewState()) + val viewState get() = _viewState.asStateFlow() + val tunnels get() = tunnelRepo.itemFlow + val state get() = vpnService.state + + val handshakeStatus get() = vpnService.handshakeStatus + val tunnelName get() = vpnService.tunnelName + private val _settings = MutableStateFlow(Settings()) + val settings get() = _settings.asStateFlow() + + private val defaultConfigName = { + "tunnel${(Math.random() * 100000).toInt()}" + } + + + init { + viewModelScope.launch { + settingsRepo.itemFlow.collect { + val settings = it.first() + validateWatcherServiceState(settings) + _settings.emit(settings) + } + } + } + + private fun validateWatcherServiceState(settings: Settings) { + val watcherState = ServiceManager.getServiceState(application.applicationContext, WireGuardConnectivityWatcherService::class.java) + if(settings.isAutoTunnelEnabled && watcherState == ServiceState.STOPPED && settings.defaultTunnel != null) { + ServiceManager.startWatcherService(application.applicationContext, settings.defaultTunnel!!) + } + } + + + fun onDelete(tunnel : TunnelConfig) { + viewModelScope.launch { + if(tunnelRepo.count() == 1L) { + ServiceManager.stopWatcherService(application.applicationContext) + val settings = settingsRepo.getAll() + if(!settings.isNullOrEmpty()) { + val setting = settings[0] + setting.defaultTunnel = null + setting.isAutoTunnelEnabled = false + setting.isAlwaysOnVpnEnabled = false + settingsRepo.save(setting) + } + } + tunnelRepo.delete(tunnel) + ShortcutsManager.removeTunnelShortcuts(application.applicationContext, tunnel) + } + } + + fun onTunnelStart(tunnelConfig : TunnelConfig) = viewModelScope.launch { + ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString()) + } + + fun onTunnelStop() { + ServiceManager.stopVpnService(application.applicationContext) + } + + 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) + saveTunnel(TunnelConfig(name = tunnelName, wgQuick = config.toWgQuickString())) + stream.close() + } catch(_: BadConfigException) { + viewModelScope.launch { + showSnackBarMessage(application.applicationContext.getString(R.string.bad_config)) + } + } + } + + private fun saveTunnel(tunnelConfig : TunnelConfig) { + viewModelScope.launch { + tunnelRepo.save(tunnelConfig) + ShortcutsManager.createTunnelShortcuts(application.applicationContext, tunnelConfig) + } + } + + @SuppressLint("Range") + private fun getFileName(context: Context, uri: Uri): String { + if (uri.scheme == "content") { + val cursor = try { + context.contentResolver.query(uri, null, null, null, null) + } catch (e : Exception) { + Timber.d("Exception getting config name") + null + } + cursor ?: return defaultConfigName() + cursor.use { + if(cursor.moveToFirst()) { + return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + } + } + } + return defaultConfigName() + } + + suspend fun showSnackBarMessage(message : String) { + _viewState.emit(_viewState.value.copy( + showSnackbarMessage = true, + snackbarMessage = message, + snackbarActionText = "Okay", + onSnackbarActionClick = { + viewModelScope.launch { + dismissSnackBar() + } + } + )) + delay(3000) + dismissSnackBar() + } + + private suspend fun dismissSnackBar() { + _viewState.emit(_viewState.value.copy( + showSnackbarMessage = false + )) + } + + private fun getNameFromFileName(fileName : String) : String { + return fileName.substring(0 , fileName.lastIndexOf('.') ) + } + + private fun getFileExtensionFromFileName(fileName : String) : String { + return try { + fileName.substring(fileName.lastIndexOf('.')) + } catch (e : Exception) { + "" + } + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt new file mode 100644 index 0000000000..4616a8fd9a --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -0,0 +1,415 @@ +package net.nymtech.nymconnect.ui.screens.settings + +import android.Manifest +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.rounded.LocationOff +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import net.nymtech.nymconnect.R +import net.nymtech.nymconnect.WireGuardAutoTunnel +import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig +import net.nymtech.nymconnect.ui.Routes +import net.nymtech.nymconnect.ui.common.ClickableIconButton +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class, + ExperimentalLayoutApi::class +) +@Composable +fun SettingsScreen( + viewModel: SettingsViewModel = hiltViewModel(), + padding: PaddingValues, + navController: NavController, + focusRequester: FocusRequester, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() } +) { + + val scope = rememberCoroutineScope() + val context = LocalContext.current + val focusManager = LocalFocusManager.current + val interactionSource = remember { MutableInteractionSource() } + + var expanded by remember { mutableStateOf(false) } + val viewState by viewModel.viewState.collectAsStateWithLifecycle() + 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() + var isLocationServicesEnabled by remember { mutableStateOf(viewModel.checkLocationServicesEnabled())} + + LaunchedEffect(viewState) { + if (viewState.showSnackbarMessage) { + val result = snackbarHostState.showSnackbar( + message = viewState.snackbarMessage, + actionLabel = viewState.snackbarActionText, + duration = SnackbarDuration.Long, + ) + when (result) { + SnackbarResult.ActionPerformed -> viewState.onSnackbarActionClick + SnackbarResult.Dismissed -> viewState.onSnackbarActionClick + } + } + } + + fun saveTrustedSSID() { + if (currentText.isNotEmpty()) { + scope.launch { + viewModel.onSaveTrustedSSID(currentText) + currentText = "" + } + } + } + + fun openSettings() { + scope.launch { + val intentSettings = + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + intentSettings.data = + Uri.fromParts("package", context.packageName, null) + context.startActivity(intentSettings) + } + } + + if(!backgroundLocationState.status.isGranted && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + Column(horizontalAlignment = Alignment.CenterHorizontally, + 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( + 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 + } + + if(!fineLocationState.status.isGranted) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + Text( + stringResource(id = R.string.precise_location_message), + textAlign = TextAlign.Center, + modifier = Modifier.padding(15.dp), + fontStyle = FontStyle.Italic + ) + Button(modifier = Modifier.focusRequester(focusRequester),onClick = { + fineLocationState.launchPermissionRequest() + }) { + Text(stringResource(id = R.string.request)) + } + + } + return + } + + if (tunnels.isEmpty()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + Text( + stringResource(R.string.one_tunnel_required), + textAlign = TextAlign.Center, + modifier = Modifier.padding(15.dp), + fontStyle = FontStyle.Italic + ) + } + return + } + if(!isLocationServicesEnabled && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + Text( + stringResource(id = R.string.location_services_not_detected), + textAlign = TextAlign.Center, + modifier = Modifier.padding(15.dp), + fontStyle = FontStyle.Italic + ) + Button(modifier = Modifier.focusRequester(focusRequester), onClick = { + val locationServicesEnabled = viewModel.checkLocationServicesEnabled() + isLocationServicesEnabled = locationServicesEnabled + if(!locationServicesEnabled) { + scope.launch { + viewModel.showSnackBarMessage(context.getString(R.string.detecting_location_services_disabled)) + } + } + }) { + Text(stringResource(id = R.string.check_again)) + } + } + return + } + val screenPadding = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 5.dp else 15.dp + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier + .fillMaxHeight(.85f) + .fillMaxWidth() + .verticalScroll(scrollState) + .clickable(indication = null, interactionSource = interactionSource) { + focusManager.clearFocus() + } + .padding(padding) else Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .clickable(indication = null, interactionSource = interactionSource) { + focusManager.clearFocus() + } + .padding(padding) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(screenPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(R.string.enable_auto_tunnel)) + Switch( + modifier = Modifier.focusRequester(focusRequester), + enabled = !settings.isAlwaysOnVpnEnabled, + checked = settings.isAutoTunnelEnabled, + onCheckedChange = { + scope.launch { + viewModel.toggleAutoTunnel() + } + } + ) + } + Text( + stringResource(id = R.string.select_tunnel), + textAlign = TextAlign.Center, + modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp) + ) + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + if(!(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)) { + expanded = !expanded }}, + modifier = Modifier.padding(start = 15.dp, top = 5.dp, bottom = 10.dp).clickable { + expanded = !expanded + }, + ) { + TextField( + enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled), + value = settings.defaultTunnel?.let { + TunnelConfig.from(it).name } + ?: "", + readOnly = true, + modifier = Modifier.menuAnchor(), + label = { Text(stringResource(R.string.tunnels)) }, + onValueChange = { }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = expanded + ) + } + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + tunnels.forEach() { tunnel -> + DropdownMenuItem( + onClick = { + scope.launch { + viewModel.onDefaultTunnelSelected(tunnel) + } + expanded = false + }, + text = { Text(text = tunnel.name) } + ) + } + } + } + Text( + stringResource(R.string.trusted_ssid), + textAlign = TextAlign.Center, + modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp) + ) + FlowRow( + modifier = Modifier.padding(screenPadding), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.SpaceEvenly + ) { + trustedSSIDs.forEach { ssid -> + ClickableIconButton(onIconClick = { + scope.launch { + viewModel.onDeleteTrustedSSID(ssid) + } + }, text = ssid, icon = Icons.Filled.Close, enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled)) + } + } + OutlinedTextField( + enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled), + value = currentText, + onValueChange = { currentText = it }, + label = { Text(stringResource(R.string.add_trusted_ssid)) }, + modifier = Modifier.padding(start = screenPadding, top = 5.dp), + maxLines = 1, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + saveTrustedSSID() + } + ), + trailingIcon = { + IconButton(onClick = { saveTrustedSSID() }) { + Icon( + imageVector = Icons.Outlined.Add, + contentDescription = if (currentText == "") stringResource(id = R.string.trusted_ssid_empty_description) else stringResource( + id = R.string.trusted_ssid_value_description + ), + tint = if(currentText == "") Color.Transparent else Color.Green + ) + } + }, + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(screenPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(R.string.tunnel_mobile_data)) + Switch( + enabled = !(settings.isAutoTunnelEnabled || settings.isAlwaysOnVpnEnabled), + checked = settings.isTunnelOnMobileDataEnabled, + onCheckedChange = { + scope.launch { + viewModel.onToggleTunnelOnMobileData() + } + } + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(screenPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(R.string.always_on_vpn_support)) + Switch( + enabled = !settings.isAutoTunnelEnabled, + checked = settings.isAlwaysOnVpnEnabled, + onCheckedChange = { + scope.launch { + viewModel.onToggleAlwaysOnVPN() + } + } + ) + } + } +} + + diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt new file mode 100644 index 0000000000..7143c1b4b6 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt @@ -0,0 +1,124 @@ +package net.nymtech.nymconnect.ui.screens.settings + +import android.app.Application +import android.content.Context +import android.location.LocationManager +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import net.nymtech.nymconnect.R +import net.nymtech.nymconnect.repository.Repository +import net.nymtech.nymconnect.service.foreground.ServiceManager +import net.nymtech.nymconnect.service.tunnel.model.Settings +import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig +import net.nymtech.nymconnect.ui.ViewState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + + +@HiltViewModel +class SettingsViewModel @Inject constructor(private val application : Application, + private val tunnelRepo : Repository, private val settingsRepo : Repository +) : ViewModel() { + + private val _trustedSSIDs = MutableStateFlow(emptyList()) + val trustedSSIDs = _trustedSSIDs.asStateFlow() + private val _settings = MutableStateFlow(Settings()) + val settings get() = _settings.asStateFlow() + val tunnels get() = tunnelRepo.itemFlow + private val _viewState = MutableStateFlow(ViewState()) + val viewState get() = _viewState.asStateFlow() + + init { + checkLocationServicesEnabled() + viewModelScope.launch { + settingsRepo.itemFlow.collect { + val settings = it.first() + _settings.emit(settings) + _trustedSSIDs.emit(settings.trustedNetworkSSIDs.toList()) + } + } + } + + suspend fun onSaveTrustedSSID(ssid: String) { + val trimmed = ssid.trim() + if (!_settings.value.trustedNetworkSSIDs.contains(trimmed)) { + _settings.value.trustedNetworkSSIDs.add(trimmed) + settingsRepo.save(_settings.value) + } else { + showSnackBarMessage("SSID already exists.") + } + } + + suspend fun onDefaultTunnelSelected(tunnelConfig: TunnelConfig) { + settingsRepo.save(_settings.value.copy( + defaultTunnel = tunnelConfig.toString() + )) + } + + suspend fun onToggleTunnelOnMobileData() { + settingsRepo.save(_settings.value.copy( + isTunnelOnMobileDataEnabled = !_settings.value.isTunnelOnMobileDataEnabled + )) + } + + suspend fun onDeleteTrustedSSID(ssid: String) { + _settings.value.trustedNetworkSSIDs.remove(ssid) + settingsRepo.save(_settings.value) + } + + suspend fun toggleAutoTunnel() { + if(_settings.value.defaultTunnel.isNullOrEmpty() && !_settings.value.isAutoTunnelEnabled) { + showSnackBarMessage(application.getString(R.string.select_tunnel_message)) + return + } + if(_settings.value.isAutoTunnelEnabled) { + ServiceManager.stopWatcherService(application) + } else { + if(_settings.value.defaultTunnel != null) { + val defaultTunnel = _settings.value.defaultTunnel + ServiceManager.startWatcherService(application, defaultTunnel!!) + } + } + settingsRepo.save(_settings.value.copy( + isAutoTunnelEnabled = !_settings.value.isAutoTunnelEnabled + )) + } + + suspend fun showSnackBarMessage(message : String) { + _viewState.emit(_viewState.value.copy( + showSnackbarMessage = true, + snackbarMessage = message, + snackbarActionText = "Okay", + onSnackbarActionClick = { + viewModelScope.launch { + dismissSnackBar() + } + } + )) + } + + private suspend fun dismissSnackBar() { + _viewState.emit(_viewState.value.copy( + showSnackbarMessage = false + )) + } + + suspend fun onToggleAlwaysOnVPN() { + if(_settings.value.defaultTunnel != null) { + _settings.emit( + _settings.value.copy(isAlwaysOnVpnEnabled = !_settings.value.isAlwaysOnVpnEnabled) + ) + settingsRepo.save(_settings.value) + } else { + showSnackBarMessage(application.getString(R.string.select_tunnel_message)) + } + } + fun checkLocationServicesEnabled() : Boolean { + val locationManager = + application.getSystemService(Context.LOCATION_SERVICE) as LocationManager + return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt new file mode 100644 index 0000000000..31fc9dbf6a --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt @@ -0,0 +1,83 @@ +package net.nymtech.nymconnect.ui.screens.support + +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import net.nymtech.nymconnect.R + +@Composable +fun SupportScreen(padding : PaddingValues, focusRequester: FocusRequester) { + + val context = LocalContext.current + + fun openWebPage(url: String) { + val webpage: Uri = Uri.parse(url) + val intent = Intent(Intent.ACTION_VIEW, webpage) + context.startActivity(intent) + } + + Column(horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .focusable() + .padding(padding)) { + Text(stringResource(R.string.support_text), textAlign = TextAlign.Center, modifier = Modifier.padding(30.dp), fontSize = 15.sp) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + IconButton(onClick = { + openWebPage(context.resources.getString(R.string.discord_url)) + }) { + Icon(imageVector = ImageVector.vectorResource(R.drawable.discord), "Discord") + } + IconButton(modifier = Modifier.focusRequester(focusRequester),onClick = { + openWebPage(context.resources.getString(R.string.github_url)) + }) { + Icon(imageVector = ImageVector.vectorResource(R.drawable.github), "Github") + } + } + Spacer(modifier = Modifier.weight(1f)) + Text(stringResource(id = R.string.privacy_policy), style = TextStyle(textDecoration = TextDecoration.Underline), + modifier = Modifier.clickable { + openWebPage(context.resources.getString(R.string.privacy_policy_url)) + }) + Text("App version: ${net.nymtech.nymconnect.BuildConfig.VERSION_NAME}", Modifier.padding(25.dp)) + } + } \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt new file mode 100644 index 0000000000..3561a75512 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Color.kt @@ -0,0 +1,17 @@ +package net.nymtech.nymconnect.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFF492532) +val virdigris = Color(0xFF5BC0BE) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFFFFFFFF) + +//status colors +val brickRed = Color(0xFFCE4257) +val pinkRed = Color(0xFFEF476F) +val mint = Color(0xFF52B788) diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt new file mode 100644 index 0000000000..647ad9f2ad --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Theme.kt @@ -0,0 +1,80 @@ +package net.nymtech.nymconnect.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + //primary = Purple80, + primary = virdigris, + secondary = virdigris, + // secondary = PurpleGrey80, + tertiary = virdigris + //tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun WireguardAutoTunnelTheme( + //force dark theme + darkTheme : Boolean = true, + //darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + //turning off dynamic color for now + dynamicColor: Boolean = false, + content: @Composable () -> Unit +) { + + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + WindowCompat.setDecorFitsSystemWindows(window, false) + window.statusBarColor = Color.Transparent.toArgb() + window.navigationBarColor = Color.Transparent.toArgb() + WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = !darkTheme + WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightNavigationBars = !darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/TransparentSystemBars.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/TransparentSystemBars.kt new file mode 100644 index 0000000000..6a1c35331c --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/TransparentSystemBars.kt @@ -0,0 +1,22 @@ +package net.nymtech.nymconnect.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.graphics.Color +import com.google.accompanist.systemuicontroller.rememberSystemUiController + +@Composable +fun TransparentSystemBars() { + val systemUiController = rememberSystemUiController() + val useDarkIcons = !isSystemInDarkTheme() + + DisposableEffect(systemUiController, useDarkIcons) { + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = useDarkIcons + ) + + onDispose {} + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt new file mode 100644 index 0000000000..6b89a8e327 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package net.nymtech.nymconnect.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt new file mode 100644 index 0000000000..aa240e6eb2 --- /dev/null +++ b/nym-vpn/android/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt @@ -0,0 +1,25 @@ +package net.nymtech.nymconnect.util + +import java.math.BigDecimal +import java.text.DecimalFormat +import java.time.Duration +import java.time.Instant + +object NumberUtils { + + private const val BYTES_IN_KB = 1024L + + fun bytesToKB(bytes : Long) : BigDecimal { + return bytes.toBigDecimal().divide(BYTES_IN_KB.toBigDecimal()) + } + + fun formatDecimalTwoPlaces(bigDecimal: BigDecimal) : String { + val df = DecimalFormat("#.##") + return df.format(bigDecimal) + } + + fun getSecondsBetweenTimestampAndNow(epoch : Long) : Long { + val time = Instant.ofEpochMilli(epoch) + return Duration.between(time, Instant.now()).seconds + } +} \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/res/drawable/discord.xml b/nym-vpn/android/app/src/main/res/drawable/discord.xml new file mode 100644 index 0000000000..df43339353 --- /dev/null +++ b/nym-vpn/android/app/src/main/res/drawable/discord.xml @@ -0,0 +1,10 @@ + + + diff --git a/nym-vpn/android/app/src/main/res/drawable/github.xml b/nym-vpn/android/app/src/main/res/drawable/github.xml new file mode 100644 index 0000000000..32f8254ebe --- /dev/null +++ b/nym-vpn/android/app/src/main/res/drawable/github.xml @@ -0,0 +1,12 @@ + + + diff --git a/nym-vpn/android/app/src/main/res/drawable/ic_launcher_background.xml b/nym-vpn/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..07d5da9cbf --- /dev/null +++ b/nym-vpn/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nym-vpn/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/nym-vpn/android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000000..2b068d1146 --- /dev/null +++ b/nym-vpn/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/res/drawable/shield.xml b/nym-vpn/android/app/src/main/res/drawable/shield.xml new file mode 100644 index 0000000000..e49a1f265d --- /dev/null +++ b/nym-vpn/android/app/src/main/res/drawable/shield.xml @@ -0,0 +1,5 @@ + + + diff --git a/nym-vpn/android/app/src/main/res/drawable/vpn_off.xml b/nym-vpn/android/app/src/main/res/drawable/vpn_off.xml new file mode 100644 index 0000000000..d33949b74b --- /dev/null +++ b/nym-vpn/android/app/src/main/res/drawable/vpn_off.xml @@ -0,0 +1,5 @@ + + + diff --git a/nym-vpn/android/app/src/main/res/drawable/vpn_on.xml b/nym-vpn/android/app/src/main/res/drawable/vpn_on.xml new file mode 100644 index 0000000000..1339fb3b8b --- /dev/null +++ b/nym-vpn/android/app/src/main/res/drawable/vpn_on.xml @@ -0,0 +1,5 @@ + + + diff --git a/nym-vpn/android/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml b/nym-vpn/android/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml new file mode 100644 index 0000000000..cf3108b364 --- /dev/null +++ b/nym-vpn/android/app/src/main/res/mipmap-anydpi-v26/ic_banner.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/nym-vpn/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..036d09bc5f --- /dev/null +++ b/nym-vpn/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/nym-vpn/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..036d09bc5f --- /dev/null +++ b/nym-vpn/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/nym-vpn/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..52206777aa3245f61ae885e0825057c0550c2f0e GIT binary patch literal 2004 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 literal 0 HcmV?d00001 diff --git a/nym-vpn/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/nym-vpn/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..106e0e13cc5996f9e412a6277c365a55550b330d GIT binary patch literal 1926 zcmV;12YL8XNk&F~2LJ$9MM6+kP&iC+2LJ#sYrq;1HPN;9|Mca|eZ;nt+O}=mw(T6- z=FBnvzwa-;fn1OFckexG?S0zhH|S3HX`|8=FJii^wPUu^#kmICcDk%oPAYY?(wViZ zL6vIyu(QqDMrYeem7H$6X_MBr*`_pZVpgssg>2il)3!=}vVUWCm?K-Z&OTC@p}ow^ z%*;$r%pkdqBuSFII;#X9Ay+8~ZQHi7c`|3)wr$(C-A#a+ ze1MX4A@*7zUGomf$xZN?0Ku1WqTgBa3c<-na3h`RWi}EF1bZfQe}&}@0;x&x3?FuH zRuK&dcIEVwY&&Bm|4R^WyO>3SO$nWR1j`8i11^-iQve8_3r@$0yq_SOA#w(icH{Ym zrj=^z6Racn0{}S+Pp_aQxF%R%(My6EnWVHZDQ77nw*Z0tKx@r~mq$|f+El2_>TBr8_=TmTU5DT$@t!9*@I9f239cRr0P|Uj*lP~HL1jq52lUn;!{)msrILQ54~sK6Y9(CT?Gyu9*oaITovtP1i&IVYxTxX8sDO6H%$?3W4z&emFRS$Ce^^WzM7^MlVuWk|8TCCQ{Oi z8F6jm>%`U=Az3|-UmU>&OA$OmOotw)BfMM@<%%p5S*>2WV+5?2Do2c9)|mZa4wdqO zOo^`#fcQp5l#3(SVW~Bcrt$2oG8>%`S7RW#JSEY!imKM&Sfl8rm(s7{4#$wkB4$>M zhg-P{AYGPU0D!nUk(J^ImRL$+4}yO+BL;}6Rdf}d!H-{R`tztN`*5nxK03Sok*fjl zj*9?F`sWJ3u6uF><18h(D~rl7N22P?!>Rt?qqd(n$7%b~k)zl!2MpZAg$1MvUIMV^ zuAEfc+)@NHb^L<*-#}`gT7TV~)}9_|y%NlzCoa`w9!~emi@RIHRA_tkN;UvRLB_}vw4`vmRb9-hK@!R#)>Eh#gUu7L=5A zbQaZnxw`)kCwx&FwEFCD`n+&q5}SmT3cxQ(nw*rZgu;rT?!ukcQUnjpC5+2?B}c>b zcJbo0|KNiuvmFeQ+C~;5SjG9}%0Se-oNE6+zz%%!;;`g_j58x2*syTZ$7f zQrTiff_vr+2Coa)ZR92?5{BUY1mYWK4@#=-%@vUhC)QeXR|rz z?=6O4va-o(|6^i#bvAi-eyzc?LtY~_RIl(0RSGa^T(sM zUyp^W7>HaH^ob2uw)qpm2~B2#k6++tiKuYPW=W%pomSMm21z4i8IkjXKEa(dk#PG^ z!lWF*UjSJt!!(x833owzi6CH5a2hH*6)+1xil&3*>kgewm@yp$FmQ~r)y!d{4+HQG z_*s|?rrpJBq~_Yor$i64SCTS|rMzSO1{uC{JL%UJA%n|1^f{4yFJo@6jJwyg=|#PzotK9*6$q*a`eBn=o!R z&~7VfYAbu*o7ih^Yvvm_yEFvuKI*1{jatJELsbvSmkW(c$cMFt83me`ViC z|D%aDD-tV3u=2_)CrAcvuW#@rVS6b%U+G!s=Mo-nf#9pqX{zk~{HyeG6rOJVe5SH{ z(MyD+-KevSo0a{7lTF!wMS_{@XilTll$ebK7s@0cPn4t$!583d7pIQOD_uxYNqbh( MZk0*0tH~@60R0}G`2YX_ literal 0 HcmV?d00001 diff --git a/nym-vpn/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/nym-vpn/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..c388f6dae5a66dd7133c1c43af5f493e7a735579 GIT binary patch literal 2242 zcmV;z2tD^wNk&Gx2mkZJ3xp?HvjcF#*l+iB`GU z{x_2H$9uQCvpXZ<2?=R}R_MQiyBld|JNP%iLwc7}1QAvIo{e$wnUoIh3Lv}`8`DBT14|`|@XYBo4*t zBX%{Y~f&bDoOkFCv|_2QVBr3|--%yKfVfCjj(4{L%BsNgh! zM-=m>hnx-^I*lZ-{|ApikZt1+@>?!d{mcB*U;r#xt}Gh9FJnHm*|rc)B}|BR(bbvt&dEKg+-jz{};aX!q7 zXx~<8{RxPd5o#!c-b1G$N%(rKmasjSOpq*PtJ&|VvhT<7It$`h8hK9F^zgnz6zT6w zPjk1bt;j2b2Md&ml}ssEIJ`5X(X6DuS#ds&;Q0SL1rJy3j}4-Jg`Byf7CMTA+KxU5 z3Sv}cbQzR-ebQz`di9Rcm7xg(01O0-3IJfk=*fP5Hhe5}7e4@O2pO*pbqH!RptX3GgOz|9K&{7Q+)ytVt!8v5;bOtb z#^68zfW)LqmWD|H@K(yqV5X4CfRRA17689QrM@$u$txVCv2AI*4GBQ?=LI1ahF>xj zp&SBZ000-N%4<*b?-T9k6h3$YVvt<*l1vmn3J@yS3V>U_622Ocz~5etTTb^>7)$dK za>{Nk0FWzqs6+780ayU-MR##=W6MIhRY2>~#v zuUxLQvtVz?r?~Iu0N+gFsUx*@SZx6BlRa7YegVI01o#Wg4Wv+uD89D{Kn=bba_#Iy z?SvKO$GkHGz-NK|HqUGUns9w zB@WpU_sz)Oc61DCj(tQeCIvwV3KRHcq6i(HACZ2KSjM(wp5n~G~w>TnFW9bb7B|sc}O5J`C|wee+{alSf8eh0?>2x;=kMdgCoJC*+>wr`i90r z079S#KpU=jZ->P4IYz+Rit~>0VGP1lUwPJIo=hpQq3S#+O=hI}W2S$m2v zKIPY!bFl!xBx;K|KbbUhTIQP1;bd6_$9d9tu|Ci6+JK2;CJR?T^F%sw!!dy(xQ+x0xlR`sgv5Bk^Ec}qq=68wHoPpjghIlm)g+R@=N5s^ zj%~wJfi61ZFD&>hFarP~xLRNd)CevvAbpwn4;0D#H))1t4a*$!7)V8Gp?QDj{DgiT znPcE^7NN9CWx~OQk^+z^HID@h2}H<^ZYZk5mu@r^p@+s22p-E@H6D^at<$#@9BRAN z7MvWYNbo{9h&36nALvTwteS5&{EiWX{yNoVQ{rb^91J*<@OJQ9%1_r#(c)duM zpFcV$NR+48mM5EFQD3?wTgnG>D33XidBGqktqQa-0EK}ClSHnwWw*8_@@tZbAmGUl z%N#ZYhG#XUrV5oL&o;O@1gYtub8ck5S|pCDNwcQytJQtd2%@|hPjBcKYyNlOn5K>11Zgg(2u9@w>uHn;ifq#+@)JfE(1i>HKQptvuc&2?3Aj}g zO-#X@rZ7oW`!O`t#2|e?(ZsFV)gI+LPQEw-lVF~+>I+Mo0=Q9AxB(!&p0yeX76+~4 zNIWhR1SG2|v)nt}63t`Us`{zbHTEsJO=jB7sjZ9SgsVRrR4zhyKz9x(M?2 z%+|9nKdjkJFdMw|xWqq~TLju*U(X2GjE2XkQ_t1Q?)CHyZn|Y-E)mz53mp04Ze1df Q3Dna|FWs`0M)6J<0KA+ky8r+H literal 0 HcmV?d00001 diff --git a/nym-vpn/android/app/src/main/res/mipmap-xhdpi/ic_banner.png b/nym-vpn/android/app/src/main/res/mipmap-xhdpi/ic_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd39f501dd7a23fb706d9c597bdec9b3c511e5e GIT binary patch literal 3578 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 BaRvYY literal 0 HcmV?d00001 diff --git a/nym-vpn/android/app/src/main/res/mipmap-xhdpi/ic_banner_foreground.png b/nym-vpn/android/app/src/main/res/mipmap-xhdpi/ic_banner_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..634417a64034f28adeff6c7c3c5b29d0a2fb2908 GIT binary patch literal 3770 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 O>pytM>i)0$zdmieM{+&@ literal 0 HcmV?d00001 diff --git a/nym-vpn/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/nym-vpn/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..c89a4abd0e7daace668cc1a0b61de43fff99bf6e GIT binary patch literal 5030 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 literal 0 HcmV?d00001 diff --git a/nym-vpn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/nym-vpn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..bf191eb8171c41c8f46e66be48b647099254f843 GIT binary patch literal 4246 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 literal 0 HcmV?d00001 diff --git a/nym-vpn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/nym-vpn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..0a5390f963768a0a39cfa58f6d30e1d0d199509e GIT binary patch literal 7718 zcmV+>9@*hiNk&E<9smGWMM6+kP&iBy9smF@kH8}kO*n`oNs*%Au;Tm+Uig;~(fy^0V#l&1Gc3_Z4e+bvjN*%4G18^Jrn`S z@Z7iU_J`VTA=Br%uhw>(z1r3y8O~GFe|+Dta2rXIBIWall&0N@2cVOZh?052r*2H-U~TA)rulDTIuNj%bT1^_O=qd91LFo#aWmMwou zCtLx5$5#O7$5oO9w$S$$L4jHnSOj&>IT2YrvNb0?SOgf_1a#f!uPWS1k|eQ%bWA`2 z020H|BuNr)KqmlxxR^ncBqL7vdc5`f3xH>7Is@SIlODhsnABbZfV58(0KdP;q_4R@ zns^}q2Ec^^faqw@_ZE*500aP(0)Ryl5rD!4Hu7QsV9OSEy70mlwz|MZaX4rjNs?mz zu)BNiK@brW&|kv`_6M`d;q^#IkJ?y-8r=gu%K8#T@9u|2r=`s0g#H+_{Qrt1yZN20 z>H#raoDrEBMavsrF}{KKei*+2t^5$o%p_)MW-fNzp{B|_i>k`ZlT~Bb{mfLRJ25I@ zn;8WaDQM$gOugtZl^Ir7T;>xYmX28J5Vm97j=Qq7n(+m*|7J=vQ;A_^wrgRGpd?AM zY^(7&_W|lP*GT){A3g6RC2QN3x6So@|1YJ?j0@n_(}GssqYb39j+vQjOaK2pl&Ebp zBw24nWT{b&vDL=dwr%dNxwEypySuV&+qT=bRaQpC1X!B>e{mzH!D`tf%a+H?gJudd zGcz;OEtPkW8@+-Yb4rfP%#6dPVKT~+Nmlov|NU3HU%@xb=<2BY(v>3(&#G!c!PD}t zYk5y$TJF$|rb?xn(}tNzVdlH9VNN>CbmHhjL6U9LwpBIvKGw5st3|K7-?pu5{e!*M zlo+;=AR+P`^j|RiCMEy~ex$o9wGFTo*_Wv*oCM))ff1%xAn(L~6p?+7DxBm$0?n3s zga>n?KYz}ueSXky%z(jXQ8c10DQhI+KP8jTkHk;%YgE`0s5^Ne_95z7k>oK7sM>5M0S0uk~g?>cZcp?j%kML0QIeX`leaa;C+ zMI@oxV#oh6R^}_izZ{MYisYA=`NZm**YUykJ;GTrNOvlO@L21-$aVoa2dN^(qQ{4o zuEk=8MwF7!8qCXy!;7Gk+ik*EU8oGgEo?Bk5^@}5lNR^z8jHIAUq&1f#Gr54 zw;_DDDOqugb*Hi4gzO^kKfVWxee2=vNzCW=wLAnqC(CZ4cdEEm*P+}2_LKL2l(P78 z>)VsyBfC7v>Q34<;G}vy=6~~%t09*tx4u0)_5P1in&YbEn9tsCcLyak`sK8dBjj@+ zik)ml--AE>2})`g@I7vESQp)#wh7Xz3z6(_v8Ky=8_sO;`a1S}$6XRh8r^H_3PRn( z%8pkf@<)kTZrDqHX=myCV0iRz=LPoILRwg3S6m}$PqrNW+vf{-KOj0tE7reRWo8LK z?@{cU#!x@X|F3r;GYpagMSHJ2=YUqIMl3D) z4fpc+d&MI@&g}gx@Q9k+75sO3Ym&&c(rgHRJd1vS0`;)KA{LnwS@KYtEvKl4Xz^kM z4;1S6Xnu?sB+VkXWh)5)mSYx4rXku!8b9wv`FjQWM^3p7n{|Rk9#WeDeXG}T@+5}a z*?_b}bA=*zVg6%bgy$t@OPEDYK^ho_q2gLUzN>%r>sn!LfDORX673PG6QvYH_#tGV zy(}>2+)8CpP&2^ZzHhHTB8;o(C&1bxkD;EpX zuvOUxHaHL{SEo`FvJ6B^%JDgd0D!Azer+eb2fS@z$3i0%ga1X$cx?TX_j3jWcew4e zr3Ub`Yh&?m+&49t~~sh6DK5Z*a#es z;wKF!xof4mf|DZnh6A>~aDn93$}hZalXry^AP(7n=y3(CQH{tZ!9Zz=;37~$nGv>y z2V5DB7xoG^!F-I*7ml?eYUKaB>`#aMsbCAMrG4V=8R!Zxb%R@?PN@ z%A9aWaw5qhtT-JcsDL-kRqzT~0R&?}Z%8NaIc7rbs3e}L(Y$i70Wl13LQ}Z=9j3yR z^1sY+&_CLG8z1Dr-**dhkh>txdWv&0m@FNXG(jIIZ{>-?>G6?*XiPdtt(@;iv3}aI ztz$kBONljQ7=1tX56*`F=s>*Z^w z(!-VihfS7l?1iD&J%|4XU2(k`-HpXqHxY|{jg5FWGH}mZzn-C*?PhGm#9vHq^ z=a#c>`hF=XVKWiSMnWHRH_`owMhw*6&K~i~<^Y_WD&6tRA2#pNs%F}LmffI-(vw?e zrG-#QNi4)ntfV?I1*y6FzjFDi2D!UFF4lsTR2Qs$Jdj1l2L`-0&{|$vydMCx-})`P zI-#rKt8;a6fpcsV(^z==wTxa}6P@2^XmL%E(H%GYkdsQ1hiODH_2*|9T_!jF4KoZK zlC6ObBR_5nM| zrP3Ym?OV(T3e`DC`Gz zk5+frUUuv=K<&#>aebx6jXva}5_3PlNz%bksq#95~;n!$M%n zY;aO1f7Oq9kiv|+2WBijzRI7gynN1n0C1eY8(wo+2;o`mrtThi7m&@4?K>sE$x_gh zAc_Jff*y~7dX*3&L{=two4#)s6>elEy|1??=eU~XU$Q6<224MUHk)tng1pOSV+q29 zxMhl)A;b*31eTWR~yA%P36mFwTyIqU9xl7?N4~ zlSy%fHC6=F_sg`lSi7o^v~JZh3*2}V#3gsg7jIRHErsX~Xa0}tKp1k4mYT$XvgQ?> z9be)2Rgi7uh!Bl%AjLTrp*a<%LpDAqQjBG{XKE$z*v4kA=5W7$I@$v%+Odpa+|v6Y z$X~oC1r5(`J>SfK0Vy8H7Hq;3R-Ai$V-P}XA~NyK6^Am%JmbjgD1V*v(hx4 zN4mENrJ9FxI4R5t=@QTt(%u6iVKHv0umWypZ^B-8y?y6;=eHM>twe&*-P^p2FQCRB zzbxWf$z4`p)=JjcSzmtSRZd}!5II1j>;ev@>eIlD(&#l;mWp@v!FgLDnfJJcd=~ zl^TTFLA0#$@R>=Wzd@3F8(+&JE!+Ku)_+ z(nR5zgh9n)0`L=!kyPU!t$?ayF*wmLMG9|-cJxsx;*hz1)Hah`s5e4A5YM98sslox z(4)2*^L+uRLs_gzk!O3j6$+UHu-JU|3fBy-&KM5j&>GETpL%lY@II^ z7{M@X6;@yqCV#6o+)Ju?SpvNm}(;sGc*EiUfd2AatF38N}2jOeCmU(Pa)y{BowaUhc@2w zqj%hUmp{&i&1m4LkEplghnw~O7vmaFW-Yep=I;ICB~J3u=RV3Fd$K)dCpl*CVsp)6 zQn*WPFy*Gicx$GQDH8_|8V+;c>vADQM4>y^*IDqbVlsh5%TEm#6dMaK1~2^LFaE_8 zX;ZE_u_jfb882ry9>v4I(Ys--ktqCn=m^71V6f-zn~dDZze_A$Ald+__5z9_4;_>L~w@S5-h)JwBr?q+r4xtLKN+5hIQt5D;a^;Xhv6Mj0zObvB(qFv#$`{f_ z$Ihd%0Wb#q)pJnxjL-||6XkOM!3GytEZYN1^OLR|T<-%KJTiq&P~K=NjiwurTNjH) zNFZ9+f)G!?N}x+b;}=6|MH*MvipybcOYgU^x6sD$4-7U4YyK^WHCYNBq;&8t!*zKF z+>~fuMf!+khw{PUL~vlm@+Y6elnpXwGew8jf-6FsrHgkbPG3j=~1(LRraB=8@FE4yuuN0o^46F zD+=@Be$i^9IEW+C>tR#%-{q7&1t-JRL`HySdw5BTrH@WGo?0#&EeJR`xn&R(e3`Hs z$wG`~@i^rOpxeI95HOFHHZZK%>EVC4z!Y|DGDj=q?*y0*=*BYNlG!`Tg=l{HvZq7^ zHxRLEYo9Qs#6~;EPBxyx^N_ZzC06+541mc6b*Hm$z}1gC;ryd`-FD6mbgQHIYD)v( z7)e|@K~lv9%F)26d?WX|ufS{AvX#p*5gU4UPpqkCI^@)DW`cE6RMA@1%I^o|rB@4d z3$+SOSzDXHccxtx_R?z^;Lyo;Ph=Th8vlrQVo}f1{rDnmV#SYLGBQU;sM?5dmL#TyKkEdObwZ`dIc)kLmGql=HeIBT=>8^Wk7`Ix z{d$mdQCbry-!ahD2%DXAge6ET3Op;S_^6@llgcV7gi_&FC8tXt9RP%jTdFzMdo@D8 zD!i+KD~{quBgnwBQrI4THlfk{n?|IgB5nT{DRzUDAUDsCr7%S6N6_@HrH+3u<2-5V zGq`UM;eLGw{*Hf>AlC`WSNwBXTSEKzw`$>3a0QX0M)@av15>IRYYIB1>uy+Ug{Jf& zL<#M8!Vg@mazTM@yN|X1XU5gJr$NP9gX30e1v*~WgpheHT73#tpccp^Mh#tO;AqKI zvZXA915CRHHq_iwc&R|wi=#xF8zf8Bb|i2niT>=2MR_zcC`h(yA$L~8_!y5WeLRv- z<9vln34h^?qgeh{;9*)V{rvJHRIiB}NZ!#Q5UFRV?YwO-?0r|Y17WN23gKVr8ftx_ zI^OkLKhZQ)o#uaQ)N<$ug*F`~VW*sE5_mxMK%;-SV1w`{UTF@!>;<8zF$!8h8x=_^8tPq;MH1-c)_ z`Xh+LV?JscD%`2%nJ1rv^qRPGsDdIgq(%s_pQ^Wg!#zvuR_x&mCW&gj^lOP_&{Df& zm7{>phd!Ci{NuCbpHQX~pm)AoZ`Z##bOpCn8tchMwSMbnt8t?GDmG`KY1AF+Dw<>4 zBTsnIa1(dmJ4)tmd=%xtG@Mf4gye8)c#W8 zxsk#=>K)IZ!KH@r2}r`DD6_(yfIAkfeG!%8p|hL(_<3NotV9d|mCdZHwZ7OD2Xi2drI5Vtbr~2UH6Q4fPfrU%%DM8 zX)Iqa*aqMco}gJsxbVQbITV%3HFrEWtzh$=tLf4-E_9`ko_#4J@EO$KZ*^)0h!>&=KYh8?f00twEVKC;|`OJr1e-ZL1dzwo&vY zUr^eAh=qiR{f5a6YGE_53FaQ)K_?+yvy4VMGprSWn-yCbP>kU2!)wpVR@LoMIN+`a z0@>1ok*tUOS|Nah7}J;*S)z#~ZWr5f=Igc0qroyT!HdO14jU0 zIL8sRxfUS)jmM@lzqk^*6xcFwD@v191E?(o-sQ6Zk?7<91mNd-QdA?uyVuthCESWn z5r9jP0DQhqUUWyT`|U@I+H!{2N4$lfCCTsCg(XRi*&|7 z3&H*=aQA+enXfte=DfGaj$$&PO&vr`+jLY-ZZr|PE+}}ncsbO3NmPPygsPS zJpX;A*CP0dDwqO+MV*0N;reNqlzngB#rHO7cF|B+>Y3gv-BHUkeQ6{}i!BIu30Dkz zg?2uhAJ^Lsn*G^d12 zo>co<{AQfu96HHhT3I9$t7#PlNK0rq_8ESKd&;Y_sH#e(O@;!&{1~$^wy8Rc(;46S84V z3ZiLQJCJOIViKyVER<5I-zULlbQLOw-9~g|kI_oLPpK@v1QBd@CFs>|GfJZE)g2@V zJ5oaB7qAP|TkU(~ICPKou^q4uG^nQ!O;R|ah*WSy8fJr?<^(MAzq*ALerD3mI zz-&MOS45~RzLL`1AeyxQX-n*p1{JzNxW`gbVh#N(CM5HYRz%pLA|j!Ji>uODe{ASK zOBOKY0*X*cgXs!ziB1WNP}-?%`XEV z)9Z!64i{DL_4E3MjM08WK~&~roT$jKs2vc*;H)I18uMMl{zgoA0rOuV*{dp%jw983 z54(h;C$U>gIAYK?a!qEP`NxCxH7%5^vy34ozEHky*k75YV*w3E0he^ISB21dq*AG# z-vkFIF>S)AG23y1*_ea`Yl;y9RSN>O5KC_%9^&a;$lLSx`os*ip92SR8b~cK_kyr% zDpg=gO|X{G+}3|T8cHEq;_7P-F5TG9Bn@k;F|vS zzkWim3eq){%_<&_DzV#|>6RNTB^%gUD{CW4BK&tZ0t3{Jq3Af5p%ptAjy*?9)=b9I z6wfd7d^>M%G_KJf_}S%ef5H=%dqLP`g%p9Ee}apg0$*;Xyk@M8&RANTBE!^`NHMU; zFeD+OPp*=@E4er%$DSPh)C&@ZtTxBmu9tSNp4@ku}2!!*=OTYID5S&KV7f zZ3GzkoS%Kn^=8~fh8e+~{V}}_>7KU_8d4&15RUdSf4#iC4BV;` zc5#t%Ql5F{WOA=F;V2)-DTJz0y~U~Hw^m=xraXa(t=ia94Uz^O0!8Fm{8_k|h-E# z2!Y*Ssor~EmcA@Y@4c$`o=RZ|K_rAwRaK~}5JC{jK7gcBRn>d1Et*M6%MgNu5JW^M g+?Ohqs?=sGn}yO$5ekL-WF@I2sU(FYg`}{f0S|+@CIA2c literal 0 HcmV?d00001 diff --git a/nym-vpn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/nym-vpn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..b8e72301e0f7b7f4efaf60a6eb7fee52662a62c9 GIT binary patch literal 5854 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 literal 0 HcmV?d00001 diff --git a/nym-vpn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/nym-vpn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..dae95ed873f77c0e88a56b5880539a0d0741908e GIT binary patch literal 8314 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 E01HmZuK)l5 literal 0 HcmV?d00001 diff --git a/nym-vpn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/nym-vpn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..e16ef0b03fa9704ca00bb07a75ff2866d3cbd07b GIT binary patch literal 10402 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 M72vGu^g0t40MXOiGynhq literal 0 HcmV?d00001 diff --git a/nym-vpn/android/app/src/main/res/values/colors.xml b/nym-vpn/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000000..d035176a0b --- /dev/null +++ b/nym-vpn/android/app/src/main/res/values/colors.xml @@ -0,0 +1,11 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF1c1b20 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/res/values/ic_banner_background.xml b/nym-vpn/android/app/src/main/res/values/ic_banner_background.xml new file mode 100644 index 0000000000..5274b8a4f3 --- /dev/null +++ b/nym-vpn/android/app/src/main/res/values/ic_banner_background.xml @@ -0,0 +1,4 @@ + + + #121212 + \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/res/values/ic_launcher_background.xml b/nym-vpn/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000000..9c1cd0cc32 --- /dev/null +++ b/nym-vpn/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #1C1B20 + \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/res/values/strings.xml b/nym-vpn/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..2c1d791d4d --- /dev/null +++ b/nym-vpn/android/app/src/main/res/values/strings.xml @@ -0,0 +1,90 @@ + + NymConnect + tunnelConfig + VPN Channel + VPN Notification Channel + Watcher Channel + Watcher Notification Channel + FOREGROUND_FILE + https://github.com/nymtech/nym + https://github.com/nymtech/nym + File is not a .conf file + Turn off tunnel before editing + No tunnels added yet! + Tunnel name already exists + https://discord.com/invite/nym + Watcher Service + Monitoring network state changes + VPN Connected + Connected to tunnel - + VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again. + Notifications permission is required for the app to work properly. + Open Settings + Add Trusted SSID + Trusted SSID + Tunnels + Select Tunnel + Enable auto tunneling + Tunnel on mobile data + \"Allow all the time\" location permission is required for retrieving Wi-Fi SSID in the background. Permission is needed for this feature. + Location permission is required for this feature to work properly. + At least one tunnel required to use this feature + "Retry" + View Privacy Policy + Okay + This feature requires background location permission to enable Wi-Fi SSID monitoring even while the application is closed. For more details, please see the Privacy Policy linked on the Support screen. + Background Location Disclosure + TODO + Enter SSID + Submit SSID + [Interface] + Add tunnel from files + File Open + Tunnel Edit + Tunnel Name + Edit + Delete + Add Tunnel + Exclude + Include + Tunnel all applications + Configuration changes saved. + Save changes + Icon + No thanks + Turn on + Map + Bad config. Please try again. + Interface + Public key + Waiting for the Barcode UI module to be downloaded. + Barcode module downloading. Try again. + Addresses + DNS servers + MTU + Peer + Allowed IPs + Endpoint + Transfer + Last handshake + Name + Restart Tunnel + VPN Connection Failed + Failed connection to - + Attempting to connect to server after 30 seconds of no response. + Attempting to reconnect to server after more than one minute of no response. + Enable Always-On VPN support + Please select a tunnel first + Unable to detect Location Services which are required for this feature. Please enable Location Services. + Check again + Detecting Location Services disabled + This feature requires precise location to access Wi-Fi SSID name. Please enable precise location here or in the app settings. + Request + Toggle VPN + No tunnels available + Search packages + Clear Icon + Search Icon + Attempting connection.. + VPN Starting + \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/res/values/themes.xml b/nym-vpn/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000000..9414a536e4 --- /dev/null +++ b/nym-vpn/android/app/src/main/res/values/themes.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/res/xml/backup_rules.xml b/nym-vpn/android/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000000..fa0f996d2c --- /dev/null +++ b/nym-vpn/android/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/nym-vpn/android/app/src/main/res/xml/data_extraction_rules.xml b/nym-vpn/android/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000000..9ee9997b0b --- /dev/null +++ b/nym-vpn/android/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/nym-vpn/android/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt b/nym-vpn/android/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt new file mode 100644 index 0000000000..c3453e018c --- /dev/null +++ b/nym-vpn/android/app/src/test/java/com/zaneschepke/wireguardautotunnel/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package net.nymtech.nymconnect + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/nym-vpn/android/build.gradle.kts b/nym-vpn/android/build.gradle.kts new file mode 100644 index 0000000000..09b1978ecb --- /dev/null +++ b/nym-vpn/android/build.gradle.kts @@ -0,0 +1,19 @@ +// 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") + } +} + +plugins { + id("com.android.application") version "8.2.0-beta01" apply false + id("org.jetbrains.kotlin.android") version "1.8.22" apply false + id("com.google.dagger.hilt.android") version "2.44" apply false + kotlin("plugin.serialization") version "1.8.22" apply false +} diff --git a/nym-vpn/android/gradle.properties b/nym-vpn/android/gradle.properties new file mode 100644 index 0000000000..1023be831f --- /dev/null +++ b/nym-vpn/android/gradle.properties @@ -0,0 +1,25 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# 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 \ No newline at end of file diff --git a/nym-vpn/android/gradle/wrapper/gradle-wrapper.jar b/nym-vpn/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/nym-vpn/android/gradle/wrapper/gradle-wrapper.properties b/nym-vpn/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..96f2e7a959 --- /dev/null +++ b/nym-vpn/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Apr 24 22:46:45 EDT 2023 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/nym-vpn/android/gradlew b/nym-vpn/android/gradlew new file mode 100755 index 0000000000..4f906e0c81 --- /dev/null +++ b/nym-vpn/android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/nym-vpn/android/gradlew.bat b/nym-vpn/android/gradlew.bat new file mode 100644 index 0000000000..107acd32c4 --- /dev/null +++ b/nym-vpn/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/nym-vpn/android/settings.gradle.kts b/nym-vpn/android/settings.gradle.kts new file mode 100644 index 0000000000..aaebe93cd6 --- /dev/null +++ b/nym-vpn/android/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "NymConnect" +include(":app") + \ No newline at end of file