Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35c99d21fc | |||
| f4e42d74c4 | |||
| 9de1e6e844 | |||
| 713df39106 | |||
| 5ec4674f9b | |||
| 58c5092e80 | |||
| 677ad54a7f | |||
| 5e17c3199f |
@@ -1,40 +0,0 @@
|
||||
name: ci-nym-vpn-ui-js
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'nym-vpn/ui/src/**'
|
||||
- 'nym-vpn/ui/package.json'
|
||||
- 'nym-vpn/ui/index.html'
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: custom-linux
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install Yarn
|
||||
run: npm install -g yarn
|
||||
- name: Install dependencies
|
||||
working-directory: nym-vpn/ui
|
||||
run: yarn
|
||||
- name: Type-check
|
||||
working-directory: nym-vpn/ui
|
||||
run: yarn typecheck
|
||||
- name: Check lint
|
||||
working-directory: nym-vpn/ui
|
||||
run: yarn lint
|
||||
- name: Check formatting
|
||||
working-directory: nym-vpn/ui
|
||||
run: yarn fmt:check
|
||||
# - name: Run tests
|
||||
# working-directory: nym-vpn/ui
|
||||
# run: yarn test
|
||||
- name: Check build
|
||||
working-directory: nym-vpn/ui
|
||||
run: yarn build
|
||||
@@ -1,63 +0,0 @@
|
||||
name: ci-nym-vpn-ui-rust
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'nym-vpn/ui/src-tauri/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: custom-linux
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGOTOML_PATH: ./nym-vpn/ui/src-tauri/Cargo.toml
|
||||
steps:
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools libayatana-appindicator3-dev
|
||||
continue-on-error: true
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Prepare build
|
||||
run: mkdir nym-vpn/ui/dist
|
||||
|
||||
- name: Build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --manifest-path ${{ env.CARGOTOML_PATH }} --features custom-protocol
|
||||
|
||||
# - name: Run all tests
|
||||
# uses: actions-rs/cargo@v1
|
||||
# with:
|
||||
# command: test
|
||||
# args: --manifest-path ${{ env.CARGOTOML_PATH }}
|
||||
|
||||
- name: Check formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --manifest-path ${{ env.CARGOTOML_PATH }} --all -- --check
|
||||
|
||||
- name: Annotate with clippy checks
|
||||
uses: actions-rs/clippy-check@v1
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --manifest-path ${{ env.CARGOTOML_PATH }} --all-features
|
||||
|
||||
- name: Clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --manifest-path ${{ env.CARGOTOML_PATH }} --all-features --all-targets -- -D warnings
|
||||
@@ -13,13 +13,13 @@ DENOMS_EXPONENT=6
|
||||
REWARDING_VALIDATOR_ADDRESS=n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa
|
||||
MIXNET_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav
|
||||
VESTING_CONTRACT_ADDRESS=n1unyuj8qnmygvzuex3dwmg9yzt9alhvyeat0uu0jedg2wj33efl5qackslz
|
||||
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l
|
||||
GROUP_CONTRACT_ADDRESS=n1pd7kfgvr5tpcv0xnlv46c4jsq9jg2r799xxrcwqdm4l2jhq2pjwqrmz5ju
|
||||
MULTISIG_CONTRACT_ADDRESS=n14ph4e660eyqz0j36zlkaey4zgzexm5twkmjlqaequxr2cjm9eprqsmad6k
|
||||
COCONUT_DKG_CONTRACT_ADDRESS=n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a
|
||||
EPHEMERA_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
|
||||
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n13902g92xfefeyzuyed49snlm5fxv5ms6mdq5kvrut27hasdw5a9q9vyw6c
|
||||
GROUP_CONTRACT_ADDRESS=n18nczmqw6adwxg2wnlef3hf0etf8anccafp2pjpul5rrtmv96umyq5mv7t5
|
||||
MULTISIG_CONTRACT_ADDRESS=n1q3zzxl78rlmxv3vn0uf4vkyz285lk8q2xzne299yt9x6mpfgk90qukuzmv
|
||||
COCONUT_DKG_CONTRACT_ADDRESS=n1jsz20ggp5a6v76j060erkzvxmeus8htlpl77yxp878f0gf95cyaq6p2pee
|
||||
NAME_SERVICE_CONTRACT_ADDRESS=n12ne7qtmdwd0j03t9t5es8md66wq4e5xg9neladrsag8fx3y89rcs36asfp
|
||||
SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS=n1ps5yutd7sufwg058qd7ac7ldnlazsvmhzqwucsfxmm445d70u8asqxpur4
|
||||
EPHEMERA_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
|
||||
|
||||
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
|
||||
EXPLORER_API=https://sandbox-explorer.nymtech.net/api
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
# 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/
|
||||
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
@@ -1,42 +0,0 @@
|
||||
## 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
|
||||
@@ -1,2 +0,0 @@
|
||||
/build
|
||||
/release
|
||||
@@ -1,159 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
{
|
||||
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
|
||||
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
|
||||
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
|
||||
"entities": [
|
||||
{
|
||||
"id": "1:2692736974585027589",
|
||||
"lastPropertyId": "15:5057486545428188436",
|
||||
"name": "TunnelConfig",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:1985347930017457084",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "12:2409068226744965585",
|
||||
"name": "name",
|
||||
"indexId": "1:4811206443952699137",
|
||||
"type": 9,
|
||||
"flags": 34848
|
||||
},
|
||||
{
|
||||
"id": "13:8987443291286312275",
|
||||
"name": "wgQuick",
|
||||
"type": 9
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
{
|
||||
"id": "2:8887605597748372702",
|
||||
"lastPropertyId": "9:4468844863383145378",
|
||||
"name": "Settings",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:7485739868216068651",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "2:5814013113141456749",
|
||||
"name": "isAutoTunnelEnabled",
|
||||
"type": 1
|
||||
},
|
||||
{
|
||||
"id": "4:5645665441196906014",
|
||||
"name": "trustedNetworkSSIDs",
|
||||
"type": 30
|
||||
},
|
||||
{
|
||||
"id": "5:4989886999117763881",
|
||||
"name": "isTunnelOnMobileDataEnabled",
|
||||
"type": 1
|
||||
},
|
||||
{
|
||||
"id": "6:3370284381040192129",
|
||||
"name": "defaultTunnel",
|
||||
"type": 9
|
||||
},
|
||||
{
|
||||
"id": "9:4468844863383145378",
|
||||
"name": "isAlwaysOnVpnEnabled",
|
||||
"type": 1
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
}
|
||||
],
|
||||
"lastEntityId": "2:8887605597748372702",
|
||||
"lastIndexId": "1:4811206443952699137",
|
||||
"lastRelationId": "0:0",
|
||||
"lastSequenceId": "0:0",
|
||||
"modelVersion": 5,
|
||||
"modelVersionParserMinimum": 5,
|
||||
"retiredEntityUids": [],
|
||||
"retiredIndexUids": [],
|
||||
"retiredPropertyUids": [
|
||||
1763475292291320186,
|
||||
6483820955437198310,
|
||||
8323071516033820771,
|
||||
5904440563612311217,
|
||||
1408037976996390989,
|
||||
7737847485212546994,
|
||||
8215616901775229364,
|
||||
8021610768066328637,
|
||||
6174306582797008721,
|
||||
2175939938544485767,
|
||||
7555225587864607050,
|
||||
969146862000617878,
|
||||
5057486545428188436,
|
||||
2814640993034665120,
|
||||
4981008812459251156
|
||||
],
|
||||
"retiredRelationUids": [],
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
{
|
||||
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
|
||||
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
|
||||
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
|
||||
"entities": [
|
||||
{
|
||||
"id": "1:2692736974585027589",
|
||||
"lastPropertyId": "15:5057486545428188436",
|
||||
"name": "TunnelConfig",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:1985347930017457084",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "12:2409068226744965585",
|
||||
"name": "name",
|
||||
"indexId": "1:4811206443952699137",
|
||||
"type": 9,
|
||||
"flags": 34848
|
||||
},
|
||||
{
|
||||
"id": "13:8987443291286312275",
|
||||
"name": "wgQuick",
|
||||
"type": 9
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
{
|
||||
"id": "2:8887605597748372702",
|
||||
"lastPropertyId": "8:4981008812459251156",
|
||||
"name": "Settings",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:7485739868216068651",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "2:5814013113141456749",
|
||||
"name": "isAutoTunnelEnabled",
|
||||
"type": 1
|
||||
},
|
||||
{
|
||||
"id": "4:5645665441196906014",
|
||||
"name": "trustedNetworkSSIDs",
|
||||
"type": 30
|
||||
},
|
||||
{
|
||||
"id": "5:4989886999117763881",
|
||||
"name": "isTunnelOnMobileDataEnabled",
|
||||
"type": 1
|
||||
},
|
||||
{
|
||||
"id": "6:3370284381040192129",
|
||||
"name": "defaultTunnel",
|
||||
"type": 9
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
}
|
||||
],
|
||||
"lastEntityId": "2:8887605597748372702",
|
||||
"lastIndexId": "1:4811206443952699137",
|
||||
"lastRelationId": "0:0",
|
||||
"lastSequenceId": "0:0",
|
||||
"modelVersion": 5,
|
||||
"modelVersionParserMinimum": 5,
|
||||
"retiredEntityUids": [],
|
||||
"retiredIndexUids": [],
|
||||
"retiredPropertyUids": [
|
||||
1763475292291320186,
|
||||
6483820955437198310,
|
||||
8323071516033820771,
|
||||
5904440563612311217,
|
||||
1408037976996390989,
|
||||
7737847485212546994,
|
||||
8215616901775229364,
|
||||
8021610768066328637,
|
||||
6174306582797008721,
|
||||
2175939938544485767,
|
||||
7555225587864607050,
|
||||
969146862000617878,
|
||||
5057486545428188436,
|
||||
2814640993034665120,
|
||||
4981008812459251156
|
||||
],
|
||||
"retiredRelationUids": [],
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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
|
||||
@@ -1,24 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
|
||||
android:maxSdkVersion="30"
|
||||
tools:ignore="LeanbackUsesWifi" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>
|
||||
<!--foreground service permissions-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<!--start service on boot permission-->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<!--android tv support-->
|
||||
<uses-feature android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.screen.portrait"
|
||||
android:required="false" />
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent>
|
||||
</queries>
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:name=".WireGuardAutoTunnel"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:banner="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.WireguardAutoTunnel"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.WireguardAutoTunnel">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:name=".service.shortcut.ShortcutsActivity"/>
|
||||
<service
|
||||
android:name=".service.foreground.ForegroundService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="remoteMessaging"
|
||||
android:exported="false">
|
||||
</service>
|
||||
<service
|
||||
android:exported="true"
|
||||
android:name=".service.tile.TunnelControlTile"
|
||||
android:icon="@drawable/shield"
|
||||
android:label="NymConnect"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
android:value="true" />
|
||||
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardTunnelService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:enabled="true"
|
||||
android:persistent="true"
|
||||
android:foregroundServiceType="remoteMessaging"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
android:value="true"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||
android:enabled="true"
|
||||
android:stopWithTask="false"
|
||||
android:persistent="true"
|
||||
android:foregroundServiceType="location"
|
||||
android:permission=""
|
||||
android:exported="false">
|
||||
</service>
|
||||
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
Before Width: | Height: | Size: 38 KiB |
@@ -1,6 +0,0 @@
|
||||
package net.nymtech.nymconnect
|
||||
|
||||
object Constants {
|
||||
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L;
|
||||
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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<Settings>
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
settingsRepo.init()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun isRunningOnAndroidTv(context : Context) : Boolean {
|
||||
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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<Settings> {
|
||||
return store.boxFor(Settings::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBoxForTunnels(store : BoxStore) : Box<TunnelConfig> {
|
||||
return store.boxFor(TunnelConfig::class.java)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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<Settings>
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideTunnelRepository(tunnelBox: TunnelBox) : Repository<TunnelConfig>
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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<WifiService>
|
||||
|
||||
@Binds
|
||||
@ServiceScoped
|
||||
abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService<MobileDataService>
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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<Settings>
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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<Settings>
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package net.nymtech.nymconnect.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface Repository<T> {
|
||||
suspend fun save(t : T)
|
||||
suspend fun saveAll(t : List<T>)
|
||||
suspend fun getById(id : Long) : T?
|
||||
suspend fun getAll() : List<T>?
|
||||
suspend fun delete(t : T) : Boolean?
|
||||
suspend fun count() : Long?
|
||||
|
||||
val itemFlow : Flow<MutableList<T>>
|
||||
|
||||
fun init()
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package 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<Settings>, private val boxStore : BoxStore) : Repository<Settings> {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val itemFlow = box.query().build().subscribe().toFlow()
|
||||
|
||||
override fun init() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if(getAll().isNullOrEmpty()) {
|
||||
save(Settings())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun save(t : Settings) {
|
||||
boxStore.awaitCallInTx {
|
||||
box.put(t)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveAll(t : List<Settings>) {
|
||||
boxStore.awaitCallInTx {
|
||||
box.put(t)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getById(id: Long): Settings? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box[id]
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAll(): List<Settings>? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box.all
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(t : Settings): Boolean? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box.remove(t)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun count() : Long? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box.count()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
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<TunnelConfig>,private val boxStore : BoxStore) : Repository<TunnelConfig> {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val itemFlow = box.query().build().subscribe().toFlow()
|
||||
override fun init() {
|
||||
|
||||
}
|
||||
|
||||
override suspend fun save(t : TunnelConfig) {
|
||||
Timber.d("Saving tunnel config")
|
||||
boxStore.awaitCallInTx {
|
||||
box.put(t)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override suspend fun saveAll(t : List<TunnelConfig>) {
|
||||
boxStore.awaitCallInTx {
|
||||
box.put(t)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getById(id: Long): TunnelConfig? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box[id]
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAll(): List<TunnelConfig>? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box.all
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(t : TunnelConfig): Boolean? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box.remove(t)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun count() : Long? {
|
||||
return boxStore.awaitCallInTx {
|
||||
box.count()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package net.nymtech.nymconnect.service.foreground
|
||||
|
||||
enum class Action {
|
||||
START,
|
||||
STOP
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
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 <T> Context.isServiceRunning(service: Class<T>) =
|
||||
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
|
||||
.getRunningServices(Integer.MAX_VALUE)
|
||||
.any { it.service.className == service.name }
|
||||
|
||||
fun <T : Service> getServiceState(context: Context, cls : Class<T>): ServiceState {
|
||||
val isServiceRunning = context.isServiceRunning(cls)
|
||||
return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
|
||||
}
|
||||
|
||||
private fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
|
||||
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
|
||||
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
|
||||
val intent = Intent(context, cls).also {
|
||||
it.action = action.name
|
||||
extras?.forEach {(k, v) ->
|
||||
it.putExtra(k, v)
|
||||
}
|
||||
}
|
||||
intent.component?.javaClass
|
||||
try {
|
||||
when(action) {
|
||||
Action.START -> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package net.nymtech.nymconnect.service.foreground
|
||||
|
||||
enum class ServiceState {
|
||||
STARTED,
|
||||
STOPPED,
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
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<WifiService>
|
||||
|
||||
@Inject
|
||||
lateinit var mobileDataService : NetworkService<MobileDataService>
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepo: Repository<Settings>
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
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<Settings>
|
||||
|
||||
@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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
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<T : BaseNetworkService<T>>(val context: Context, networkCapability : Int) : NetworkService<T> {
|
||||
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 <Result> Flow<NetworkStatus>.map(
|
||||
crossinline onUnavailable: suspend (network : Network) -> Result,
|
||||
crossinline onAvailable: suspend (network : Network) -> Result,
|
||||
crossinline onCapabilitiesChanged: suspend (network : Network, networkCapabilities : NetworkCapabilities) -> Result,
|
||||
): Flow<Result> = map { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
||||
is NetworkStatus.Available -> onAvailable(status.network)
|
||||
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(status.network, status.networkCapabilities)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR) {
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package net.nymtech.nymconnect.service.network
|
||||
|
||||
import android.net.NetworkCapabilities
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NetworkService<T> {
|
||||
fun getNetworkName(networkCapabilities: NetworkCapabilities) : String?
|
||||
val networkStatus : Flow<NetworkStatus>
|
||||
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI) {
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
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<String,String>) : Intent {
|
||||
return Intent(context, ShortcutsActivity::class.java).also {
|
||||
it.action = Action.START.name
|
||||
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
|
||||
extras.forEach {(k, v) ->
|
||||
it.putExtra(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTunnelOffIntent(context : Context, extras : Map<String,String>) : Intent {
|
||||
return Intent(context, ShortcutsActivity::class.java).also {
|
||||
it.action = Action.STOP.name
|
||||
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
|
||||
extras.forEach {(k, v) ->
|
||||
it.putExtra(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package 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<Settings>
|
||||
|
||||
@Inject
|
||||
lateinit var configRepo : Repository<TunnelConfig>
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService : VpnService
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main);
|
||||
|
||||
private lateinit var job : Job
|
||||
|
||||
override fun onStartListening() {
|
||||
job = scope.launch {
|
||||
updateTileState()
|
||||
}
|
||||
super.onStartListening()
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
qsTile.contentDescription = this.resources.getString(R.string.toggle_vpn)
|
||||
scope.launch {
|
||||
updateTileState();
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
super.onTileRemoved()
|
||||
cancelJob()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
scope.launch {
|
||||
try {
|
||||
val tunnel = determineTileTunnel();
|
||||
if(tunnel != null) {
|
||||
attemptWatcherServiceToggle(tunnel.toString())
|
||||
if(vpnService.getState() == Tunnel.State.UP) {
|
||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||
} else {
|
||||
ServiceManager.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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<Tunnel.State>
|
||||
val tunnelName : SharedFlow<String>
|
||||
val statistics : SharedFlow<Statistics>
|
||||
val lastHandshake : SharedFlow<Map<Key,Long>>
|
||||
val handshakeStatus : SharedFlow<HandshakeStatus>
|
||||
fun getState() : Tunnel.State
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
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<Tunnel.State>(
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
replay = 1)
|
||||
|
||||
private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
override val state get() = _state.asSharedFlow()
|
||||
|
||||
private val _statistics = MutableSharedFlow<Statistics>(replay = 1)
|
||||
override val statistics get() = _statistics.asSharedFlow()
|
||||
|
||||
private val _lastHandshake = MutableSharedFlow<Map<Key, Long>>(replay = 1)
|
||||
override val lastHandshake get() = _lastHandshake.asSharedFlow()
|
||||
|
||||
override val handshakeStatus: SharedFlow<HandshakeStatus>
|
||||
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<Key, Long>()
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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<String> = mutableListOf(),
|
||||
var defaultTunnel : String? = null,
|
||||
var isAlwaysOnVpnEnabled : Boolean = false,
|
||||
)
|
||||
@@ -1,89 +0,0 @@
|
||||
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<String>, wgQuick: String) : String {
|
||||
if(packages.isEmpty()) {
|
||||
return wgQuick
|
||||
}
|
||||
val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick)
|
||||
val excludeConfig = buildExcludedApplicationsString(packages)
|
||||
return addApplicationsToConfig(excludeConfig, clearedWgQuick)
|
||||
}
|
||||
|
||||
fun setIncludedApplicationsOnQuick(packages : List<String>, 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>) : String {
|
||||
return EXCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
|
||||
}
|
||||
|
||||
private fun buildIncludedApplicationsString(packages : List<String>) : String {
|
||||
return INCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
|
||||
}
|
||||
fun from(string : String) : TunnelConfig {
|
||||
return Json.decodeFromString<TunnelConfig>(string)
|
||||
}
|
||||
fun configFromQuick(wgQuick: String): Config {
|
||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
||||
return Config.parse(reader)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
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")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
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
|
||||
)
|
||||
@@ -1,31 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
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)
|
||||
)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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<BottomNavItem>) {
|
||||
|
||||
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",
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
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,
|
||||
)
|
||||
@@ -1,241 +0,0 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
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<TunnelConfig>,
|
||||
private val settingsRepo : Repository<Settings>) : ViewModel() {
|
||||
|
||||
private val _tunnel = MutableStateFlow<TunnelConfig?>(null)
|
||||
private val _tunnelName = MutableStateFlow("")
|
||||
val tunnelName get() = _tunnelName.asStateFlow()
|
||||
val tunnel get() = _tunnel.asStateFlow()
|
||||
private val _packages = MutableStateFlow(emptyList<PackageInfo>())
|
||||
val packages get() = _packages.asStateFlow()
|
||||
private val packageManager = application.packageManager
|
||||
|
||||
private val _checkedPackages = MutableStateFlow(mutableStateListOf<String>())
|
||||
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<PackageInfo> {
|
||||
return getPackagesHoldingPermissions(arrayOf(Manifest.permission.INTERNET))
|
||||
}
|
||||
|
||||
private fun getPackagesHoldingPermissions(permissions: Array<String>): List<PackageInfo> {
|
||||
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()
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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<TunnelConfig>, private val vpnService : VpnService
|
||||
|
||||
) : ViewModel() {
|
||||
|
||||
private val _tunnel = MutableStateFlow<Config?>(null)
|
||||
val tunnel get() = _tunnel.asStateFlow()
|
||||
|
||||
private val _tunnelName = MutableStateFlow<String>("")
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
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<TunnelConfig?>(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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
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<TunnelConfig>,
|
||||
private val settingsRepo : Repository<Settings>,
|
||||
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) {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,415 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
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<TunnelConfig>, private val settingsRepo : Repository<Settings>
|
||||
) : ViewModel() {
|
||||
|
||||
private val _trustedSSIDs = MutableStateFlow(emptyList<String>())
|
||||
val trustedSSIDs = _trustedSSIDs.asStateFlow()
|
||||
private val _settings = MutableStateFlow(Settings())
|
||||
val settings get() = _settings.asStateFlow()
|
||||
val tunnels get() = tunnelRepo.itemFlow
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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)
|
||||
@@ -1,80 +0,0 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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
|
||||
)
|
||||
*/
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="800dp"
|
||||
android:height="800dp"
|
||||
android:viewportWidth="256"
|
||||
android:viewportHeight="256">
|
||||
<path
|
||||
android:pathData="M216.86,45.1C200.29,37.34 182.57,31.71 164.04,28.5C161.77,32.61 159.11,38.15 157.28,42.55C137.58,39.58 118.07,39.58 98.74,42.55C96.91,38.15 94.19,32.61 91.9,28.5C73.35,31.71 55.61,37.36 39.04,45.14C5.62,95.65 -3.44,144.9 1.09,193.46C23.26,210.01 44.74,220.07 65.86,226.65C71.08,219.47 75.73,211.84 79.74,203.8C72.1,200.9 64.79,197.32 57.89,193.17C59.72,191.81 61.51,190.39 63.24,188.93C105.37,208.63 151.13,208.63 192.75,188.93C194.51,190.39 196.3,191.81 198.11,193.17C191.18,197.34 183.85,200.92 176.22,203.82C180.23,211.84 184.86,219.49 190.1,226.67C211.24,220.09 232.74,210.03 254.91,193.46C260.23,137.17 245.83,88.37 216.86,45.1ZM85.47,163.59C72.83,163.59 62.46,151.79 62.46,137.41C62.46,123.04 72.61,111.21 85.47,111.21C98.34,111.21 108.71,123.02 108.49,137.41C108.51,151.79 98.34,163.59 85.47,163.59ZM170.53,163.59C157.88,163.59 147.51,151.79 147.51,137.41C147.51,123.04 157.66,111.21 170.53,111.21C183.39,111.21 193.76,123.02 193.54,137.41C193.54,151.79 183.39,163.59 170.53,163.59Z"
|
||||
android:fillColor="#5865F2"
|
||||
android:fillType="nonZero"/>
|
||||
</vector>
|
||||
@@ -1,12 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="800dp"
|
||||
android:height="800dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M10,0C15.523,0 20,4.59 20,10.253C20,14.782 17.138,18.624 13.167,19.981C12.66,20.082 12.48,19.762 12.48,19.489C12.48,19.151 12.492,18.047 12.492,16.675C12.492,15.719 12.172,15.095 11.813,14.777C14.04,14.523 16.38,13.656 16.38,9.718C16.38,8.598 15.992,7.684 15.35,6.966C15.454,6.707 15.797,5.664 15.252,4.252C15.252,4.252 14.414,3.977 12.505,5.303C11.706,5.076 10.85,4.962 10,4.958C9.15,4.962 8.295,5.076 7.497,5.303C5.586,3.977 4.746,4.252 4.746,4.252C4.203,5.664 4.546,6.707 4.649,6.966C4.01,7.684 3.619,8.598 3.619,9.718C3.619,13.646 5.954,14.526 8.175,14.785C7.889,15.041 7.63,15.493 7.54,16.156C6.97,16.418 5.522,16.871 4.63,15.304C4.63,15.304 4.101,14.319 3.097,14.247C3.097,14.247 2.122,14.234 3.029,14.87C3.029,14.87 3.684,15.185 4.139,16.37C4.139,16.37 4.726,18.2 7.508,17.58C7.513,18.437 7.522,19.245 7.522,19.489C7.522,19.76 7.338,20.077 6.839,19.982C2.865,18.627 0,14.783 0,10.253C0,4.59 4.478,0 10,0"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
||||
@@ -1,170 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -1,30 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z"/>
|
||||
</vector>
|
||||
@@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20.83,18H21v-4h2v-4H12.83L20.83,18zM19.78,22.61l1.41,-1.41L2.81,2.81L1.39,4.22l2.59,2.59C2.2,7.85 1,9.79 1,12c0,3.31 2.69,6 6,6c2.21,0 4.15,-1.2 5.18,-2.99L19.78,22.61zM8.99,11.82C9,11.88 9,11.94 9,12c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2s0.9,-2 2,-2c0.06,0 0.12,0 0.18,0.01L8.99,11.82z"/>
|
||||
</vector>
|
||||
@@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
|
||||
</vector>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black_background">#FF1c1b20</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_banner_background">#121212</color>
|
||||
</resources>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#1D2125</color>
|
||||
</resources>
|
||||
@@ -1,90 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">NymConnect</string>
|
||||
<string name="tunnel_extras_key">tunnelConfig</string>
|
||||
<string name="vpn_channel_id">VPN Channel</string>
|
||||
<string name="vpn_channel_name">VPN Notification Channel</string>
|
||||
<string name="watcher_channel_id">Watcher Channel</string>
|
||||
<string name="watcher_channel_name">Watcher Notification Channel</string>
|
||||
<string name="foreground_file">FOREGROUND_FILE</string>
|
||||
<string name="github_url">https://github.com/nymtech/nym</string>
|
||||
<string name="privacy_policy_url">https://github.com/nymtech/nym</string>
|
||||
<string name="file_extension_message">File is not a .conf file</string>
|
||||
<string name="turn_off_tunnel">Turn off tunnel before editing</string>
|
||||
<string name="no_tunnels">No tunnels added yet!</string>
|
||||
<string name="tunnel_exists">Tunnel name already exists</string>
|
||||
<string name="discord_url">https://discord.com/invite/nym</string>
|
||||
<string name="watcher_notification_title">Watcher Service</string>
|
||||
<string name="watcher_notification_text">Monitoring network state changes</string>
|
||||
<string name="tunnel_start_title">VPN Connected</string>
|
||||
<string name="tunnel_start_text">Connected to tunnel -</string>
|
||||
<string name="vpn_permission_required">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.</string>
|
||||
<string name="notification_permission_required">Notifications permission is required for the app to work properly.</string>
|
||||
<string name="open_settings">Open Settings</string>
|
||||
<string name="add_trusted_ssid">Add Trusted SSID</string>
|
||||
<string name="trusted_ssid">Trusted SSID</string>
|
||||
<string name="tunnels">Tunnels</string>
|
||||
<string name="select_tunnel">Select Tunnel</string>
|
||||
<string name="enable_auto_tunnel">Enable auto tunneling</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="background_location_reason">\"Allow all the time\" location permission is required for retrieving Wi-Fi SSID in the background. Permission is needed for this feature.</string>
|
||||
<string name="location_permission_reason">Location permission is required for this feature to work properly.</string>
|
||||
<string name="one_tunnel_required">At least one tunnel required to use this feature</string>
|
||||
<string name="retry">"Retry"</string>
|
||||
<string name="privacy_policy">View Privacy Policy</string>
|
||||
<string name="okay">Okay</string>
|
||||
<string name="prominent_background_location_message">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.</string>
|
||||
<string name="prominent_background_location_title">Background Location Disclosure</string>
|
||||
<string name="support_text">TODO</string>
|
||||
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
||||
<string name="trusted_ssid_value_description">Submit SSID</string>
|
||||
<string name="config_validation">[Interface]</string>
|
||||
<string name="add_from_file">Add tunnel from files</string>
|
||||
<string name="open_file">File Open</string>
|
||||
<string name="tunnel_edit">Tunnel Edit</string>
|
||||
<string name="tunnel_name">Tunnel Name</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="add_tunnel">Add Tunnel</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
<string name="include">Include</string>
|
||||
<string name="tunnel_all">Tunnel all applications</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="save_changes">Save changes</string>
|
||||
<string name="icon">Icon</string>
|
||||
<string name="no_thanks">No thanks</string>
|
||||
<string name="turn_on">Turn on</string>
|
||||
<string name="map">Map</string>
|
||||
<string name="bad_config">Bad config. Please try again.</string>
|
||||
<string name="config_interface">Interface</string>
|
||||
<string name="public_key">Public key</string>
|
||||
<string name="barcode_downloading">Waiting for the Barcode UI module to be downloaded.</string>
|
||||
<string name="barcode_downloading_message">Barcode module downloading. Try again.</string>
|
||||
<string name="addresses">Addresses</string>
|
||||
<string name="dns_servers">DNS servers</string>
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="peer">Peer</string>
|
||||
<string name="allowed_ips">Allowed IPs</string>
|
||||
<string name="endpoint">Endpoint</string>
|
||||
<string name="transfer">Transfer</string>
|
||||
<string name="last_handshake">Last handshake</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="restart">Restart Tunnel</string>
|
||||
<string name="vpn_connection_failed">VPN Connection Failed</string>
|
||||
<string name="failed_connection_to">Failed connection to -</string>
|
||||
<string name="initial_connection_failure_message">Attempting to connect to server after 30 seconds of no response.</string>
|
||||
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
|
||||
<string name="always_on_vpn_support">Enable Always-On VPN support</string>
|
||||
<string name="select_tunnel_message">Please select a tunnel first</string>
|
||||
<string name="location_services_not_detected">Unable to detect Location Services which are required for this feature. Please enable Location Services.</string>
|
||||
<string name="check_again">Check again</string>
|
||||
<string name="detecting_location_services_disabled">Detecting Location Services disabled</string>
|
||||
<string name="precise_location_message">This feature requires precise location to access Wi-Fi SSID name. Please enable precise location here or in the app settings.</string>
|
||||
<string name="request">Request</string>
|
||||
<string name="toggle_vpn">Toggle VPN</string>
|
||||
<string name="no_tunnel_available">No tunnels available</string>
|
||||
<string name="hint_search_packages">Search packages</string>
|
||||
<string name="clear_icon">Clear Icon</string>
|
||||
<string name="search_icon">Search Icon</string>
|
||||
<string name="attempt_connection">Attempting connection..</string>
|
||||
<string name="vpn_starting">VPN Starting</string>
|
||||
</resources>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.WireguardAutoTunnel" parent="@style/Theme.AppCompat.NoActionBar">
|
||||
<item name="android:windowBackground">@color/black_background</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older that API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||