mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e395740c71 |
@@ -1,2 +0,0 @@
|
|||||||
ko_fi: zaneschepke
|
|
||||||
liberapay: zaneschepke
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
# name of the workflow
|
# name of the workflow
|
||||||
name: Android CI Tag Deployment (Pre-release)
|
name: Android CI Tag Deployment
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*.*.*-**'
|
- '*.*.*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -14,13 +13,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
KEY_STORE_PATH: ${{ secrets.KEY_STORE_PATH }}
|
||||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||||
KEY_STORE_FILE: 'android_keystore.jks'
|
|
||||||
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
|
||||||
GH_USER: ${{ secrets.GH_USER }}
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -39,16 +35,10 @@ jobs:
|
|||||||
id: decode_keystore
|
id: decode_keystore
|
||||||
uses: timheuer/base64-to-file@v1.2
|
uses: timheuer/base64-to-file@v1.2
|
||||||
with:
|
with:
|
||||||
fileName: ${{ env.KEY_STORE_FILE }}
|
fileName: 'android_keystore.jks'
|
||||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
fileDir: ${{ github.workspace }}/app/keystore/
|
||||||
encodedString: ${{ secrets.KEYSTORE }}
|
encodedString: ${{ secrets.KEYSTORE }}
|
||||||
|
|
||||||
# create keystore path for gradle to read
|
|
||||||
- name: Create keystore path env var
|
|
||||||
run: |
|
|
||||||
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
|
|
||||||
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Create service_account.json
|
- name: Create service_account.json
|
||||||
id: createServiceAccount
|
id: createServiceAccount
|
||||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||||
@@ -62,13 +52,10 @@ jobs:
|
|||||||
- name: Get apk path
|
- name: Get apk path
|
||||||
id: apk-path
|
id: apk-path
|
||||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
||||||
- name: Get version code
|
|
||||||
run: |
|
|
||||||
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
|
|
||||||
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
|
|
||||||
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v4.3.1
|
uses: actions/upload-artifact@v4.0.0
|
||||||
with:
|
with:
|
||||||
name: wgtunnel
|
name: wgtunnel
|
||||||
path: ${{ steps.apk-path.outputs.path }}
|
path: ${{ steps.apk-path.outputs.path }}
|
||||||
@@ -78,22 +65,23 @@ jobs:
|
|||||||
name: wgtunnel
|
name: wgtunnel
|
||||||
- name: Create Release with Fastlane changelog notes
|
- name: Create Release with Fastlane changelog notes
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
# fix hardcode changelog file name
|
# fix hardcode changelog file name
|
||||||
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
|
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/33400.txt
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
name: ${{ github.ref_name }}
|
name: Release ${{ github.ref_name }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: true
|
prerelease: false
|
||||||
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||||
- name: Deploy with fastlane
|
- name: Deploy with fastlane
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.2' # Not needed with a .ruby-version file
|
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
||||||
- name: Distribute app to Beta track 🚀
|
- name: Distribute app to Beta track 🚀
|
||||||
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
||||||
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
# name of the workflow
|
|
||||||
name: Android CI Tag Deployment (Release)
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- '*.*.*'
|
|
||||||
- '!*.*.*-**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build Signed APK
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
env:
|
|
||||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
|
||||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
|
||||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
|
||||||
KEY_STORE_FILE: 'android_keystore.jks'
|
|
||||||
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
|
||||||
GH_USER: ${{ secrets.GH_USER }}
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'temurin'
|
|
||||||
java-version: '17'
|
|
||||||
cache: gradle
|
|
||||||
- name: Grant execute permission for gradlew
|
|
||||||
run: chmod +x gradlew
|
|
||||||
|
|
||||||
# Here we need to decode keystore.jks from base64 string and place it
|
|
||||||
# in the folder specified in the release signing configuration
|
|
||||||
- name: Decode Keystore
|
|
||||||
id: decode_keystore
|
|
||||||
uses: timheuer/base64-to-file@v1.2
|
|
||||||
with:
|
|
||||||
fileName: ${{ env.KEY_STORE_FILE }}
|
|
||||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
|
||||||
encodedString: ${{ secrets.KEYSTORE }}
|
|
||||||
|
|
||||||
# create keystore path for gradle to read
|
|
||||||
- name: Create keystore path env var
|
|
||||||
run: |
|
|
||||||
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
|
|
||||||
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Create service_account.json
|
|
||||||
id: createServiceAccount
|
|
||||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
|
||||||
|
|
||||||
# Build and sign APK ("-x test" argument is used to skip tests)
|
|
||||||
# add fdroid flavor for apk upload
|
|
||||||
- name: Build Fdroid Release APK
|
|
||||||
run: ./gradlew :app:assembleFdroidRelease -x test
|
|
||||||
|
|
||||||
# get fdroid flavor release apk path
|
|
||||||
- name: Get apk path
|
|
||||||
id: apk-path
|
|
||||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
|
||||||
- name: Get version code
|
|
||||||
run: |
|
|
||||||
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
|
|
||||||
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
|
|
||||||
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
|
||||||
- name: Upload APK
|
|
||||||
uses: actions/upload-artifact@v4.3.1
|
|
||||||
with:
|
|
||||||
name: wgtunnel
|
|
||||||
path: ${{ steps.apk-path.outputs.path }}
|
|
||||||
- name: Download APK from build
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: wgtunnel
|
|
||||||
- name: Repository Dispatch for my F-Droid repo
|
|
||||||
uses: peter-evans/repository-dispatch@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.PAT }}
|
|
||||||
repository: zaneschepke/fdroid
|
|
||||||
event-type: fdroid-update
|
|
||||||
- name: Create Release with Fastlane changelog notes
|
|
||||||
id: create_release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
|
|
||||||
tag_name: ${{ github.ref_name }}
|
|
||||||
name: ${{ github.ref_name }}
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
|
||||||
- name: Deploy with fastlane
|
|
||||||
uses: ruby/setup-ruby@v1
|
|
||||||
with:
|
|
||||||
ruby-version: '3.2' # Not needed with a .ruby-version file
|
|
||||||
bundler-cache: true
|
|
||||||
- name: Distribute app to Prod track 🚀
|
|
||||||
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane production)
|
|
||||||
|
|
||||||
@@ -13,9 +13,16 @@ WG Tunnel
|
|||||||
|
|
||||||
|
|
||||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
||||||
|
[](https://www.amazon.com/gp/product/B0CFGGL7WK)
|
||||||
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://ko-fi.com/N4N8NMJN2)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -62,8 +69,7 @@ and on while on different networks. This app was created to offer a free solutio
|
|||||||
|
|
||||||
## Docs (WIP)
|
## Docs (WIP)
|
||||||
|
|
||||||
Basic documentation of the feature and behaviors of this app can be
|
Basic documentation of the feature and behaviors of this app can be found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
||||||
found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
|
||||||
|
|
||||||
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
|
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
|
||||||
|
|
||||||
@@ -72,11 +78,6 @@ The repository for these docs can be found [here](https://github.com/zaneschepke
|
|||||||
```
|
```
|
||||||
$ git clone https://github.com/zaneschepke/wgtunnel
|
$ git clone https://github.com/zaneschepke/wgtunnel
|
||||||
$ cd wgtunnel
|
$ cd wgtunnel
|
||||||
```
|
|
||||||
|
|
||||||
And then build the app:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ ./gradlew assembleDebug
|
$ ./gradlew assembleDebug
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+11
-9
@@ -4,7 +4,7 @@ plugins {
|
|||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.hilt.android)
|
alias(libs.plugins.hilt.android)
|
||||||
alias(libs.plugins.kotlinxSerialization)
|
id("org.jetbrains.kotlin.plugin.serialization")
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,8 @@ android {
|
|||||||
create("general") {
|
create("general") {
|
||||||
dimension = Constants.TYPE
|
dimension = Constants.TYPE
|
||||||
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
|
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
|
||||||
//any plugins general specific
|
apply(plugin = "com.google.gms.google-services")
|
||||||
|
apply(plugin = "com.google.firebase.crashlytics")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,12 +133,9 @@ android {
|
|||||||
val generalImplementation by configurations
|
val generalImplementation by configurations
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation(project(":logcatter"))
|
|
||||||
|
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
// helpers for implementing LifecycleOwner in a Service
|
// optional - helpers for implementing LifecycleOwner in a Service
|
||||||
implementation(libs.androidx.lifecycle.service)
|
implementation(libs.androidx.lifecycle.service)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
@@ -158,14 +156,13 @@ dependencies {
|
|||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
debugImplementation(libs.androidx.compose.manifest)
|
debugImplementation(libs.androidx.compose.manifest)
|
||||||
|
|
||||||
// get tunnel lib from github packages or mavenLocal
|
// wg
|
||||||
implementation(libs.tunnel)
|
implementation(libs.tunnel)
|
||||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
|
|
||||||
// logging
|
// logging
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
|
|
||||||
|
|
||||||
// compose navigation
|
// compose navigation
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
@@ -175,6 +172,7 @@ dependencies {
|
|||||||
ksp(libs.hilt.android.compiler)
|
ksp(libs.hilt.android.compiler)
|
||||||
|
|
||||||
// accompanist
|
// accompanist
|
||||||
|
implementation(libs.accompanist.systemuicontroller)
|
||||||
implementation(libs.accompanist.permissions)
|
implementation(libs.accompanist.permissions)
|
||||||
implementation(libs.accompanist.flowlayout)
|
implementation(libs.accompanist.flowlayout)
|
||||||
implementation(libs.accompanist.drawablepainter)
|
implementation(libs.accompanist.drawablepainter)
|
||||||
@@ -195,13 +193,17 @@ dependencies {
|
|||||||
// serialization
|
// serialization
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
|
// firebase crashlytics
|
||||||
|
generalImplementation(platform(libs.firebase.bom))
|
||||||
|
generalImplementation(libs.google.firebase.crashlytics.ktx)
|
||||||
|
generalImplementation(libs.google.firebase.analytics.ktx)
|
||||||
|
|
||||||
// barcode scanning
|
// barcode scanning
|
||||||
implementation(libs.zxing.android.embedded)
|
implementation(libs.zxing.android.embedded)
|
||||||
implementation(libs.zxing.core)
|
implementation(libs.zxing.core)
|
||||||
|
|
||||||
// bio
|
// bio
|
||||||
implementation(libs.androidx.biometric.ktx)
|
implementation(libs.androidx.biometric.ktx)
|
||||||
implementation(libs.pin.lock.compose)
|
|
||||||
|
|
||||||
// shortcuts
|
// shortcuts
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "328300975830",
|
||||||
|
"project_id": "wireguard-auto-tunnel",
|
||||||
|
"storage_bucket": "wireguard-auto-tunnel.appspot.com"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:328300975830:android:82cd774598ccb7234b1b77",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.zaneschepke.wireguardautotunnel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "328300975830-m72lc3hr69ddhdqh9ngr27rvc8o0jb2d.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyBsSMY0LlckizXDnuYBy7nXWGSdl8zZedI"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "328300975830-m72lc3hr69ddhdqh9ngr27rvc8o0jb2d.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 6,
|
|
||||||
"identityHash": "625820076477aca948536f7bccccc7ca",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "Settings",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `default_tunnel` TEXT, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_battery_saver_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAutoTunnelEnabled",
|
|
||||||
"columnName": "is_tunnel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
|
||||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "trustedNetworkSSIDs",
|
|
||||||
"columnName": "trusted_network_ssids",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "defaultTunnel",
|
|
||||||
"columnName": "default_tunnel",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
|
||||||
"columnName": "is_always_on_vpn_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
|
||||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isShortcutsEnabled",
|
|
||||||
"columnName": "is_shortcuts_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isBatterySaverEnabled",
|
|
||||||
"columnName": "is_battery_saver_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnWifiEnabled",
|
|
||||||
"columnName": "is_tunnel_on_wifi_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isKernelEnabled",
|
|
||||||
"columnName": "is_kernel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isRestoreOnBootEnabled",
|
|
||||||
"columnName": "is_restore_on_boot_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isMultiTunnelEnabled",
|
|
||||||
"columnName": "is_multi_tunnel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAutoTunnelPaused",
|
|
||||||
"columnName": "is_auto_tunnel_paused",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isPingEnabled",
|
|
||||||
"columnName": "is_ping_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "TunnelConfig",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wgQuick",
|
|
||||||
"columnName": "wg_quick",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_TunnelConfig_name",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '625820076477aca948536f7bccccc7ca')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 7,
|
|
||||||
"identityHash": "e65e4e7cf01f50fb03196d47b54288b1",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "Settings",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAutoTunnelEnabled",
|
|
||||||
"columnName": "is_tunnel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnMobileDataEnabled",
|
|
||||||
"columnName": "is_tunnel_on_mobile_data_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "trustedNetworkSSIDs",
|
|
||||||
"columnName": "trusted_network_ssids",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAlwaysOnVpnEnabled",
|
|
||||||
"columnName": "is_always_on_vpn_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnEthernetEnabled",
|
|
||||||
"columnName": "is_tunnel_on_ethernet_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isShortcutsEnabled",
|
|
||||||
"columnName": "is_shortcuts_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isTunnelOnWifiEnabled",
|
|
||||||
"columnName": "is_tunnel_on_wifi_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isKernelEnabled",
|
|
||||||
"columnName": "is_kernel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isRestoreOnBootEnabled",
|
|
||||||
"columnName": "is_restore_on_boot_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isMultiTunnelEnabled",
|
|
||||||
"columnName": "is_multi_tunnel_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isAutoTunnelPaused",
|
|
||||||
"columnName": "is_auto_tunnel_paused",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isPingEnabled",
|
|
||||||
"columnName": "is_ping_enabled",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "TunnelConfig",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "wgQuick",
|
|
||||||
"columnName": "wg_quick",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "tunnelNetworks",
|
|
||||||
"columnName": "tunnel_networks",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "''"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isMobileDataTunnel",
|
|
||||||
"columnName": "is_mobile_data_tunnel",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isPrimaryTunnel",
|
|
||||||
"columnName": "is_primary_tunnel",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true,
|
|
||||||
"defaultValue": "false"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_TunnelConfig_name",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e65e4e7cf01f50fb03196d47b54288b1')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ import androidx.room.testing.MigrationTestHelper
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
import com.zaneschepke.wireguardautotunnel.data.Queries
|
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@@ -23,13 +22,39 @@ class MigrationTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun migrate6To7() {
|
fun migrate4To5() {
|
||||||
helper.createDatabase(dbName, 6).apply {
|
helper.createDatabase(dbName, 4).apply {
|
||||||
// Database has schema version 1. Insert some data using SQL queries.
|
// Database has schema version 1. Insert some data using SQL queries.
|
||||||
// You can't use DAO classes because they expect the latest schema.
|
// You can't use DAO classes because they expect the latest schema.
|
||||||
execSQL(Queries.createDefaultSettings())
|
|
||||||
execSQL(
|
execSQL(
|
||||||
Queries.createTunnelConfig(),
|
"INSERT INTO Settings (is_tunnel_enabled," +
|
||||||
|
"is_tunnel_on_mobile_data_enabled," +
|
||||||
|
"trusted_network_ssids," +
|
||||||
|
"default_tunnel," +
|
||||||
|
"is_always_on_vpn_enabled," +
|
||||||
|
"is_tunnel_on_ethernet_enabled," +
|
||||||
|
"is_shortcuts_enabled," +
|
||||||
|
"is_battery_saver_enabled," +
|
||||||
|
"is_tunnel_on_wifi_enabled," +
|
||||||
|
"is_kernel_enabled," +
|
||||||
|
"is_restore_on_boot_enabled," +
|
||||||
|
"is_multi_tunnel_enabled)" +
|
||||||
|
" VALUES " +
|
||||||
|
"('false'," +
|
||||||
|
"'false'," +
|
||||||
|
"'[trustedSSID1,trustedSSID2]'," +
|
||||||
|
"'defaultTunnel'," +
|
||||||
|
"'false'," +
|
||||||
|
"'false'," +
|
||||||
|
"'false'," +
|
||||||
|
"'false'," +
|
||||||
|
"'false'," +
|
||||||
|
"'false'," +
|
||||||
|
"'false'," +
|
||||||
|
"'false')",
|
||||||
|
)
|
||||||
|
execSQL(
|
||||||
|
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
|
||||||
)
|
)
|
||||||
// Prepare for the next version.
|
// Prepare for the next version.
|
||||||
close()
|
close()
|
||||||
@@ -37,7 +62,7 @@ class MigrationTest {
|
|||||||
|
|
||||||
// Re-open the database with version 2 and provide
|
// Re-open the database with version 2 and provide
|
||||||
// MIGRATION_1_2 as the migration process.
|
// MIGRATION_1_2 as the migration process.
|
||||||
helper.runMigrationsAndValidate(dbName, 7, true)
|
helper.runMigrationsAndValidate(dbName, 5, true)
|
||||||
// MigrationTestHelper automatically verifies the schema changes,
|
// MigrationTestHelper automatically verifies the schema changes,
|
||||||
// but you need to validate that the data was migrated properly.
|
// but you need to validate that the data was migrated properly.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<!--foreground service exempt android 14-->
|
<!--foreground service exempt android 14-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
|
||||||
<!--foreground service permissions-->
|
<!--foreground service permissions-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
@@ -83,8 +84,7 @@
|
|||||||
android:screenOrientation="fullSensor"
|
android:screenOrientation="fullSensor"
|
||||||
android:stateNotNeeded="true"
|
android:stateNotNeeded="true"
|
||||||
android:theme="@style/zxing_CaptureTheme"
|
android:theme="@style/zxing_CaptureTheme"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden"
|
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||||
tools:ignore="DiscouragedApi" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".service.shortcut.ShortcutsActivity"
|
android:name=".service.shortcut.ShortcutsActivity"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
@@ -96,30 +96,13 @@
|
|||||||
android:name=".service.foreground.ForegroundService"
|
android:name=".service.foreground.ForegroundService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="systemExempted"
|
android:foregroundServiceType="systemExempted|specialUse"
|
||||||
tools:node="merge" />
|
tools:node="merge" />
|
||||||
<service
|
<service
|
||||||
android:name=".service.tile.TunnelControlTile"
|
android:name=".service.tile.TunnelControlTile"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/shield"
|
||||||
android:label="Tunnel control"
|
android:label="WG Tunnel"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
|
||||||
android:value="true" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
|
||||||
android:value="true" />
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
<service
|
|
||||||
android:name=".service.tile.AutoTunnelControlTile"
|
|
||||||
android:exported="true"
|
|
||||||
android:icon="@drawable/ic_launcher"
|
|
||||||
android:label="Auto-tunnel"
|
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||||
@@ -136,7 +119,7 @@
|
|||||||
android:name=".service.foreground.WireGuardTunnelService"
|
android:name=".service.foreground.WireGuardTunnelService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="systemExempted"
|
android:foregroundServiceType="systemExempted|specialUse"
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
android:persistent="true"
|
android:persistent="true"
|
||||||
tools:node="merge">
|
tools:node="merge">
|
||||||
@@ -151,7 +134,7 @@
|
|||||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="systemExempted"
|
android:foregroundServiceType="systemExempted|specialUse"
|
||||||
android:persistent="true"
|
android:persistent="true"
|
||||||
android:stopWithTask="false"
|
android:stopWithTask="false"
|
||||||
tools:node="merge" />
|
tools:node="merge" />
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 38 KiB |
@@ -2,23 +2,18 @@ package com.zaneschepke.wireguardautotunnel
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class WireGuardAutoTunnel : Application() {
|
class WireGuardAutoTunnel : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
instance = this
|
instance = this
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
PinManager.initialize(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -29,18 +24,11 @@ class WireGuardAutoTunnel : Application() {
|
|||||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestTunnelTileServiceStateUpdate(context: Context) {
|
fun requestTileServiceStateUpdate() {
|
||||||
TileService.requestListeningState(
|
TileService.requestListeningState(
|
||||||
context,
|
instance,
|
||||||
ComponentName(instance, TunnelControlTile::class.java),
|
ComponentName(instance, TunnelControlTile::class.java),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestAutoTunnelTileServiceUpdate(context: Context) {
|
|
||||||
TileService.requestListeningState(
|
|
||||||
context,
|
|
||||||
ComponentName(instance, AutoTunnelControlTile::class.java),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,38 +2,27 @@ package com.zaneschepke.wireguardautotunnel.data
|
|||||||
|
|
||||||
import androidx.room.AutoMigration
|
import androidx.room.AutoMigration
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.DeleteColumn
|
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import androidx.room.migration.AutoMigrationSpec
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Settings::class, TunnelConfig::class],
|
entities = [Settings::class, TunnelConfig::class],
|
||||||
version = 7,
|
version = 5,
|
||||||
autoMigrations =
|
autoMigrations =
|
||||||
[
|
[
|
||||||
AutoMigration(from = 1, to = 2),
|
AutoMigration(from = 1, to = 2),
|
||||||
AutoMigration(from = 2, to = 3),
|
AutoMigration(from = 2, to = 3),
|
||||||
AutoMigration(
|
AutoMigration(
|
||||||
from = 3,
|
from = 3,
|
||||||
to = 4,
|
to = 4,
|
||||||
),
|
),
|
||||||
AutoMigration(
|
AutoMigration(
|
||||||
from = 4,
|
from = 4,
|
||||||
to = 5,
|
to = 5,
|
||||||
),
|
),
|
||||||
AutoMigration(
|
],
|
||||||
from = 5,
|
|
||||||
to = 6,
|
|
||||||
),
|
|
||||||
AutoMigration(
|
|
||||||
from = 6,
|
|
||||||
to = 7,
|
|
||||||
spec = RemoveLegacySettingColumnsMigration::class,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
@TypeConverters(DatabaseListConverters::class)
|
@TypeConverters(DatabaseListConverters::class)
|
||||||
@@ -42,13 +31,3 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
|
|
||||||
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
abstract fun tunnelConfigDoa(): TunnelConfigDao
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteColumn(
|
|
||||||
tableName = "Settings",
|
|
||||||
columnName = "default_tunnel",
|
|
||||||
)
|
|
||||||
@DeleteColumn(
|
|
||||||
tableName = "Settings",
|
|
||||||
columnName = "is_battery_saver_enabled",
|
|
||||||
)
|
|
||||||
class RemoveLegacySettingColumnsMigration : AutoMigrationSpec
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data
|
|
||||||
|
|
||||||
import androidx.room.RoomDatabase
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class DatabaseCallback : RoomDatabase.Callback() {
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = db.run {
|
|
||||||
// Notice non-ui thread is here
|
|
||||||
beginTransaction()
|
|
||||||
try {
|
|
||||||
execSQL(Queries.createDefaultSettings())
|
|
||||||
Timber.i("Bootstrapping settings data")
|
|
||||||
setTransactionSuccessful()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
} finally {
|
|
||||||
endTransaction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -12,7 +12,7 @@ class DatabaseListConverters {
|
|||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun stringToList(value: String): MutableList<String> {
|
fun stringToList(value: String): MutableList<String> {
|
||||||
if (value.isBlank() || value.isEmpty()) return mutableListOf()
|
if (value.isEmpty()) return mutableListOf()
|
||||||
return try {
|
return try {
|
||||||
Json.decodeFromString<MutableList<String>>(value)
|
Json.decodeFromString<MutableList<String>>(value)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data
|
|
||||||
|
|
||||||
object Queries {
|
|
||||||
fun createDefaultSettings(): String {
|
|
||||||
return """
|
|
||||||
INSERT INTO Settings (is_tunnel_enabled,
|
|
||||||
is_tunnel_on_mobile_data_enabled,
|
|
||||||
trusted_network_ssids,
|
|
||||||
is_always_on_vpn_enabled,
|
|
||||||
is_tunnel_on_ethernet_enabled,
|
|
||||||
is_shortcuts_enabled,
|
|
||||||
is_tunnel_on_wifi_enabled,
|
|
||||||
is_kernel_enabled,
|
|
||||||
is_restore_on_boot_enabled,
|
|
||||||
is_multi_tunnel_enabled)
|
|
||||||
VALUES
|
|
||||||
('false',
|
|
||||||
'false',
|
|
||||||
'sampleSSID1,sampleSSID2',
|
|
||||||
'false',
|
|
||||||
'false',
|
|
||||||
'false',
|
|
||||||
'false',
|
|
||||||
'false',
|
|
||||||
'false',
|
|
||||||
'false')
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createTunnelConfig(): String {
|
|
||||||
return """
|
|
||||||
INSERT INTO TunnelConfig (name, wg_quick) VALUES ('test', 'test')
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,27 +10,19 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface SettingsDao {
|
interface SettingsDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings)
|
||||||
suspend fun save(t: Settings)
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<Settings>)
|
||||||
suspend fun saveAll(t: List<Settings>)
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings WHERE id=:id")
|
@Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings?
|
||||||
suspend fun getById(id: Long): Settings?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings")
|
@Query("SELECT * FROM settings") suspend fun getAll(): List<Settings>
|
||||||
suspend fun getAll(): List<Settings>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings LIMIT 1")
|
@Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
|
||||||
fun getSettingsFlow(): Flow<Settings>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM settings")
|
@Query("SELECT * FROM settings") fun getAllFlow(): Flow<MutableList<Settings>>
|
||||||
fun getAllFlow(): Flow<MutableList<Settings>>
|
|
||||||
|
|
||||||
@Delete
|
@Delete suspend fun delete(t: Settings)
|
||||||
suspend fun delete(t: Settings)
|
|
||||||
|
|
||||||
@Query("SELECT COUNT('id') FROM settings")
|
@Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long
|
||||||
suspend fun count(): Long
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,44 +6,21 @@ import androidx.room.Insert
|
|||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface TunnelConfigDao {
|
interface TunnelConfigDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig)
|
||||||
suspend fun save(t: TunnelConfig)
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
|
||||||
suspend fun saveAll(t: TunnelConfigs)
|
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
@Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
|
||||||
suspend fun getById(id: Long): TunnelConfig?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig")
|
@Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List<TunnelConfig>
|
||||||
suspend fun getAll(): TunnelConfigs
|
|
||||||
|
|
||||||
@Delete
|
@Delete suspend fun delete(t: TunnelConfig)
|
||||||
suspend fun delete(t: TunnelConfig)
|
|
||||||
|
|
||||||
@Query("SELECT COUNT('id') FROM TunnelConfig")
|
@Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
|
||||||
suspend fun count(): Long
|
|
||||||
|
|
||||||
@Query("SELECT * FROM TunnelConfig WHERE tunnel_networks LIKE '%' || :name || '%'")
|
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
||||||
suspend fun findByTunnelNetworkName(name: String): TunnelConfigs
|
|
||||||
|
|
||||||
@Query("UPDATE TunnelConfig SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
|
|
||||||
fun resetPrimaryTunnel()
|
|
||||||
|
|
||||||
@Query("UPDATE TunnelConfig SET is_mobile_data_tunnel = 0 WHERE is_mobile_data_tunnel =1")
|
|
||||||
fun resetMobileDataTunnel()
|
|
||||||
|
|
||||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_primary_tunnel=1")
|
|
||||||
suspend fun findByPrimary(): TunnelConfigs
|
|
||||||
|
|
||||||
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
|
|
||||||
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
|
||||||
|
|
||||||
@Query("SELECT * FROM tunnelconfig")
|
|
||||||
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-40
@@ -4,67 +4,35 @@ import android.content.Context
|
|||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.intPreferencesKey
|
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class DataStoreManager(private val context: Context) {
|
class DataStoreManager(private val context: Context) {
|
||||||
companion object {
|
companion object {
|
||||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_SHOWN")
|
||||||
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
val BATTERY_OPTIMIZE_DISABLE_SHOWN = booleanPreferencesKey("BATTERY_OPTIMIZE_DISABLE_SHOWN")
|
||||||
val TUNNEL_RUNNING_FROM_MANUAL_START =
|
|
||||||
booleanPreferencesKey("TUNNEL_RUNNING_FROM_MANUAL_START")
|
|
||||||
val ACTIVE_TUNNEL = intPreferencesKey("ACTIVE_TUNNEL")
|
|
||||||
val CURRENT_SSID = stringPreferencesKey("CURRENT_SSID")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// preferences
|
// preferences
|
||||||
private val preferencesKey = "preferences"
|
private val preferencesKey = "preferences"
|
||||||
private val Context.dataStore by
|
private val Context.dataStore by
|
||||||
preferencesDataStore(
|
preferencesDataStore(
|
||||||
name = preferencesKey,
|
name = preferencesKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun init() {
|
suspend fun init() {
|
||||||
try {
|
context.dataStore.data.first()
|
||||||
context.dataStore.data.first()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) {
|
|
||||||
try {
|
|
||||||
context.dataStore.edit { it[key] = value }
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e(e)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun <T> saveToDataStore(key: Preferences.Key<T>, value: T) =
|
||||||
|
context.dataStore.edit { it[key] = value }
|
||||||
|
|
||||||
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
||||||
|
|
||||||
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
|
suspend fun <T> getFromStore(key: Preferences.Key<T>) =
|
||||||
return try {
|
context.dataStore.data.first { it.contains(key) }[key]
|
||||||
context.dataStore.data.map { it[key] }.first()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e(e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
|
|
||||||
context.dataStore.data.map { it[key] }.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
|
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.model
|
|
||||||
|
|
||||||
data class GeneralState(
|
|
||||||
val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
|
||||||
val batteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
|
||||||
val tunnelRunningFromManualStart: Boolean = TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
|
||||||
val activeTunnelId: Int? = null
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false
|
|
||||||
const val BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT = false
|
|
||||||
const val TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,47 +7,57 @@ import androidx.room.PrimaryKey
|
|||||||
@Entity
|
@Entity
|
||||||
data class Settings(
|
data class Settings(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
@ColumnInfo(name = "is_tunnel_enabled") val isAutoTunnelEnabled: Boolean = false,
|
@ColumnInfo(name = "is_tunnel_enabled") var isAutoTunnelEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
@ColumnInfo(name = "is_tunnel_on_mobile_data_enabled")
|
||||||
val isTunnelOnMobileDataEnabled: Boolean = false,
|
var isTunnelOnMobileDataEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "trusted_network_ssids")
|
@ColumnInfo(name = "trusted_network_ssids")
|
||||||
val trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||||
@ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false,
|
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
||||||
|
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
|
||||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
||||||
val isTunnelOnEthernetEnabled: Boolean = false,
|
var isTunnelOnEthernetEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_shortcuts_enabled",
|
name = "is_shortcuts_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
val isShortcutsEnabled: Boolean = false,
|
var isShortcutsEnabled: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_battery_saver_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isBatterySaverEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_tunnel_on_wifi_enabled",
|
name = "is_tunnel_on_wifi_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
val isTunnelOnWifiEnabled: Boolean = false,
|
var isTunnelOnWifiEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_kernel_enabled",
|
name = "is_kernel_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
val isKernelEnabled: Boolean = false,
|
var isKernelEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_restore_on_boot_enabled",
|
name = "is_restore_on_boot_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
val isRestoreOnBootEnabled: Boolean = false,
|
var isRestoreOnBootEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_multi_tunnel_enabled",
|
name = "is_multi_tunnel_enabled",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
val isMultiTunnelEnabled: Boolean = false,
|
var isMultiTunnelEnabled: Boolean = false,
|
||||||
@ColumnInfo(
|
@ColumnInfo(
|
||||||
name = "is_auto_tunnel_paused",
|
name = "is_auto_tunnel_paused",
|
||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
val isAutoTunnelPaused: Boolean = false,
|
var isAutoTunnelPaused: Boolean = false,
|
||||||
@ColumnInfo(
|
) {
|
||||||
name = "is_ping_enabled",
|
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
||||||
defaultValue = "false",
|
return if (defaultTunnel != null) {
|
||||||
)
|
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
|
||||||
val isPingEnabled: Boolean = false,
|
(tunnelConfig.id == defaultConfig.id)
|
||||||
)
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,30 +5,26 @@ import androidx.room.Entity
|
|||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@Entity(indices = [Index(value = ["name"], unique = true)])
|
@Entity(indices = [Index(value = ["name"], unique = true)])
|
||||||
|
@Serializable
|
||||||
data class TunnelConfig(
|
data class TunnelConfig(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
@ColumnInfo(name = "name") val name: String,
|
@ColumnInfo(name = "name") var name: String,
|
||||||
@ColumnInfo(name = "wg_quick") val wgQuick: String,
|
@ColumnInfo(name = "wg_quick") var wgQuick: String
|
||||||
@ColumnInfo(
|
|
||||||
name = "tunnel_networks",
|
|
||||||
defaultValue = "",
|
|
||||||
)
|
|
||||||
val tunnelNetworks: MutableList<String> = mutableListOf(),
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_mobile_data_tunnel",
|
|
||||||
defaultValue = "false",
|
|
||||||
)
|
|
||||||
val isMobileDataTunnel: Boolean = false,
|
|
||||||
@ColumnInfo(
|
|
||||||
name = "is_primary_tunnel",
|
|
||||||
defaultValue = "false",
|
|
||||||
)
|
|
||||||
val isPrimaryTunnel: Boolean = false,
|
|
||||||
) {
|
) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return Json.encodeToString(serializer(), this)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
fun from(string: String): TunnelConfig {
|
||||||
|
return Json.decodeFromString<TunnelConfig>(string)
|
||||||
|
}
|
||||||
|
|
||||||
fun configFromQuick(wgQuick: String): Config {
|
fun configFromQuick(wgQuick: String): Config {
|
||||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||||
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
||||||
|
|||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
|
|
||||||
interface AppDataRepository {
|
|
||||||
suspend fun getPrimaryOrFirstTunnel(): TunnelConfig?
|
|
||||||
suspend fun getStartTunnelConfig(): TunnelConfig?
|
|
||||||
|
|
||||||
suspend fun toggleWatcherServicePause()
|
|
||||||
|
|
||||||
val settings: SettingsRepository
|
|
||||||
val tunnels: TunnelConfigRepository
|
|
||||||
val appState: AppStateRepository
|
|
||||||
}
|
|
||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class AppDataRoomRepository @Inject constructor(
|
|
||||||
override val settings: SettingsRepository,
|
|
||||||
override val tunnels: TunnelConfigRepository,
|
|
||||||
override val appState: AppStateRepository
|
|
||||||
) : AppDataRepository {
|
|
||||||
override suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? {
|
|
||||||
return tunnels.findPrimary().firstOrNull() ?: tunnels.getAll().firstOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getStartTunnelConfig(): TunnelConfig? {
|
|
||||||
return if (appState.isTunnelRunningFromManualStart()) {
|
|
||||||
appState.getActiveTunnelId()?.let {
|
|
||||||
tunnels.getById(it)
|
|
||||||
}
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun toggleWatcherServicePause() {
|
|
||||||
val settings = settings.getSettings()
|
|
||||||
if (settings.isAutoTunnelEnabled) {
|
|
||||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
|
||||||
this.settings.save(
|
|
||||||
settings.copy(
|
|
||||||
isAutoTunnelPaused = pauseAutoTunnel,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-26
@@ -1,26 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface AppStateRepository {
|
|
||||||
suspend fun isLocationDisclosureShown(): Boolean
|
|
||||||
suspend fun setLocationDisclosureShown(shown: Boolean)
|
|
||||||
|
|
||||||
suspend fun isBatteryOptimizationDisableShown(): Boolean
|
|
||||||
suspend fun setBatteryOptimizationDisableShown(shown: Boolean)
|
|
||||||
|
|
||||||
suspend fun isTunnelRunningFromManualStart(): Boolean
|
|
||||||
suspend fun setTunnelRunningFromManualStart(id: Int)
|
|
||||||
|
|
||||||
suspend fun setManualStop()
|
|
||||||
|
|
||||||
suspend fun getActiveTunnelId(): Int?
|
|
||||||
|
|
||||||
suspend fun getCurrentSsid(): String?
|
|
||||||
|
|
||||||
suspend fun setCurrentSsid(ssid: String)
|
|
||||||
|
|
||||||
val generalStateFlow: Flow<GeneralState>
|
|
||||||
|
|
||||||
}
|
|
||||||
-81
@@ -1,81 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class DataStoreAppStateRepository(private val dataStoreManager: DataStoreManager) :
|
|
||||||
AppStateRepository {
|
|
||||||
override suspend fun isLocationDisclosureShown(): Boolean {
|
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
|
|
||||||
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setLocationDisclosureShown(shown: Boolean) {
|
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun isBatteryOptimizationDisableShown(): Boolean {
|
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
|
|
||||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
|
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun isTunnelRunningFromManualStart(): Boolean {
|
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START)
|
|
||||||
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setTunnelRunningFromManualStart(id: Int) {
|
|
||||||
setTunnelRunningFromManualStart(true)
|
|
||||||
setActiveTunnelId(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setManualStop() {
|
|
||||||
setTunnelRunningFromManualStart(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun setTunnelRunningFromManualStart(running: Boolean) {
|
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START, running)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getActiveTunnelId(): Int? {
|
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.ACTIVE_TUNNEL)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun setActiveTunnelId(id: Int) {
|
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.ACTIVE_TUNNEL, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getCurrentSsid(): String? {
|
|
||||||
return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setCurrentSsid(ssid: String) {
|
|
||||||
dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val generalStateFlow: Flow<GeneralState> =
|
|
||||||
dataStoreManager.preferencesFlow.map { prefs ->
|
|
||||||
prefs?.let { pref ->
|
|
||||||
try {
|
|
||||||
GeneralState(
|
|
||||||
locationDisclosureShown = pref[DataStoreManager.LOCATION_DISCLOSURE_SHOWN]
|
|
||||||
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT,
|
|
||||||
batteryOptimizationDisableShown = pref[DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN]
|
|
||||||
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
|
|
||||||
tunnelRunningFromManualStart = pref[DataStoreManager.TUNNEL_RUNNING_FROM_MANUAL_START]
|
|
||||||
?: GeneralState.TUNNELING_RUNNING_FROM_MANUAL_START_DEFAULT,
|
|
||||||
)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
Timber.e(e)
|
|
||||||
GeneralState()
|
|
||||||
}
|
|
||||||
} ?: GeneralState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-68
@@ -1,68 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.data.repository
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
class RoomTunnelConfigRepository(private val tunnelConfigDao: TunnelConfigDao) :
|
|
||||||
TunnelConfigRepository {
|
|
||||||
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
|
||||||
return tunnelConfigDao.getAllFlow()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getAll(): TunnelConfigs {
|
|
||||||
return tunnelConfigDao.getAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun save(tunnelConfig: TunnelConfig) {
|
|
||||||
tunnelConfigDao.save(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?) {
|
|
||||||
tunnelConfigDao.resetPrimaryTunnel()
|
|
||||||
tunnelConfig?.let {
|
|
||||||
save(
|
|
||||||
it.copy(
|
|
||||||
isPrimaryTunnel = true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?) {
|
|
||||||
tunnelConfigDao.resetMobileDataTunnel()
|
|
||||||
tunnelConfig?.let {
|
|
||||||
save(
|
|
||||||
it.copy(
|
|
||||||
isMobileDataTunnel = true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(tunnelConfig: TunnelConfig) {
|
|
||||||
tunnelConfigDao.delete(tunnelConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getById(id: Int): TunnelConfig? {
|
|
||||||
return tunnelConfigDao.getById(id.toLong())
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun count(): Int {
|
|
||||||
return tunnelConfigDao.count().toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByTunnelNetworksName(name: String): TunnelConfigs {
|
|
||||||
return tunnelConfigDao.findByTunnelNetworkName(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByMobileDataTunnel(): TunnelConfigs {
|
|
||||||
return tunnelConfigDao.findByMobileDataTunnel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findPrimary(): TunnelConfigs {
|
|
||||||
return tunnelConfigDao.findByPrimary()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -4,7 +4,7 @@ import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
|||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
|
class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRepository {
|
||||||
|
|
||||||
override suspend fun save(settings: Settings) {
|
override suspend fun save(settings: Settings) {
|
||||||
settingsDoa.save(settings)
|
settingsDoa.save(settings)
|
||||||
-12
@@ -12,19 +12,7 @@ interface TunnelConfigRepository {
|
|||||||
|
|
||||||
suspend fun save(tunnelConfig: TunnelConfig)
|
suspend fun save(tunnelConfig: TunnelConfig)
|
||||||
|
|
||||||
suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?)
|
|
||||||
|
|
||||||
suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
|
|
||||||
|
|
||||||
suspend fun delete(tunnelConfig: TunnelConfig)
|
suspend fun delete(tunnelConfig: TunnelConfig)
|
||||||
|
|
||||||
suspend fun getById(id: Int): TunnelConfig?
|
|
||||||
|
|
||||||
suspend fun count(): Int
|
suspend fun count(): Int
|
||||||
|
|
||||||
suspend fun findByTunnelNetworksName(name: String): TunnelConfigs
|
|
||||||
|
|
||||||
suspend fun findByMobileDataTunnel(): TunnelConfigs
|
|
||||||
|
|
||||||
suspend fun findPrimary(): TunnelConfigs
|
|
||||||
}
|
}
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.data.repository
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class TunnelConfigRepositoryImpl(private val tunnelConfigDao: TunnelConfigDao) :
|
||||||
|
TunnelConfigRepository {
|
||||||
|
override fun getTunnelConfigsFlow(): Flow<TunnelConfigs> {
|
||||||
|
return tunnelConfigDao.getAllFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAll(): TunnelConfigs {
|
||||||
|
return tunnelConfigDao.getAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun save(tunnelConfig: TunnelConfig) {
|
||||||
|
tunnelConfigDao.save(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(tunnelConfig: TunnelConfig) {
|
||||||
|
tunnelConfigDao.delete(tunnelConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun count(): Int {
|
||||||
|
return tunnelConfigDao.count().toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import android.content.Context
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -19,12 +18,11 @@ class DatabaseModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||||
return Room.databaseBuilder(
|
return Room.databaseBuilder(
|
||||||
context,
|
context,
|
||||||
AppDatabase::class.java,
|
AppDatabase::class.java,
|
||||||
context.getString(R.string.db_name),
|
context.getString(R.string.db_name),
|
||||||
)
|
)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.addCallback(DatabaseCallback())
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,4 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||||||
|
|
||||||
import javax.inject.Qualifier
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
@Qualifier
|
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Kernel
|
||||||
@Retention(AnnotationRetention.BINARY)
|
|
||||||
annotation class Kernel
|
|
||||||
|
|||||||
@@ -5,14 +5,10 @@ import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
|||||||
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRoomRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.DataStoreAppStateRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomSettingsRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.RoomTunnelConfigRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepositoryImpl
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositoryImpl
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -38,13 +34,13 @@ class RepositoryModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
||||||
return RoomTunnelConfigRepository(tunnelConfigDao)
|
return TunnelConfigRepositoryImpl(tunnelConfigDao)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
||||||
return RoomSettingsRepository(settingsDao)
|
return SettingsRepositoryImpl(settingsDao)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -52,22 +48,4 @@ class RepositoryModule {
|
|||||||
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
||||||
return DataStoreManager(context)
|
return DataStoreManager(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideGeneralStateRepository(dataStoreManager: DataStoreManager): AppStateRepository {
|
|
||||||
return DataStoreAppStateRepository(dataStoreManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideAppDataRepository(
|
|
||||||
settingsRepository: SettingsRepository,
|
|
||||||
tunnelConfigRepository: TunnelConfigRepository,
|
|
||||||
appStateRepository: AppStateRepository
|
|
||||||
): AppDataRepository {
|
|
||||||
return AppDataRoomRepository(settingsRepository, tunnelConfigRepository, appStateRepository)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import com.wireguard.android.backend.GoBackend
|
|||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
import com.wireguard.android.util.ToolsInstaller
|
import com.wireguard.android.util.ToolsInstaller
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
@@ -45,14 +44,8 @@ class TunnelModule {
|
|||||||
fun provideVpnService(
|
fun provideVpnService(
|
||||||
@Userspace userspaceBackend: Backend,
|
@Userspace userspaceBackend: Backend,
|
||||||
@Kernel kernelBackend: Backend,
|
@Kernel kernelBackend: Backend,
|
||||||
appDataRepository: AppDataRepository
|
settingsRepository: SettingsRepository
|
||||||
): VpnService {
|
): VpnService {
|
||||||
return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository)
|
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository)
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideServiceManager(appDataRepository: AppDataRepository): ServiceManager {
|
|
||||||
return ServiceManager(appDataRepository)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,4 @@ package com.zaneschepke.wireguardautotunnel.module
|
|||||||
|
|
||||||
import javax.inject.Qualifier
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
@Qualifier
|
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
|
||||||
@Retention(AnnotationRetention.BINARY)
|
|
||||||
annotation class Userspace
|
|
||||||
|
|||||||
@@ -3,43 +3,21 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var appDataRepository: AppDataRepository
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var serviceManager: ServiceManager
|
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||||
context?.run {
|
if (settingsRepository.getSettings().isAutoTunnelEnabled) {
|
||||||
val settings = appDataRepository.settings.getSettings()
|
ServiceManager.startWatcherServiceForeground(context!!)
|
||||||
if (settings.isAutoTunnelEnabled) {
|
|
||||||
Timber.i("Starting watcher service from boot")
|
|
||||||
serviceManager.startWatcherServiceForeground(context)
|
|
||||||
}
|
|
||||||
if (appDataRepository.appState.isTunnelRunningFromManualStart()) {
|
|
||||||
appDataRepository.appState.getActiveTunnelId()?.let {
|
|
||||||
Timber.i("Starting tunnel that was active before reboot")
|
|
||||||
serviceManager.startVpnServiceForeground(
|
|
||||||
context,
|
|
||||||
appDataRepository.tunnels.getById(it)?.id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (settings.isAlwaysOnVpnEnabled) {
|
|
||||||
Timber.i("Starting vpn service from boot AOVPN")
|
|
||||||
serviceManager.startVpnServiceForeground(context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-12
@@ -10,25 +10,20 @@ import com.zaneschepke.wireguardautotunnel.util.goAsync
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NotificationActionReceiver : BroadcastReceiver() {
|
class NotificationActionReceiver : BroadcastReceiver() {
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var serviceManager: ServiceManager
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||||
try {
|
try {
|
||||||
//TODO fix for manual start changes when enabled
|
val settings = settingsRepository.getSettings()
|
||||||
serviceManager.stopVpnService(context)
|
if (settings.defaultTunnel != null) {
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
ServiceManager.stopVpnService(context)
|
||||||
serviceManager.startVpnServiceForeground(context)
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
} catch (e: Exception) {
|
ServiceManager.startVpnServiceForeground(context, settings.defaultTunnel.toString())
|
||||||
Timber.e(e)
|
}
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-6
@@ -4,7 +4,6 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
open class ForegroundService : LifecycleService() {
|
open class ForegroundService : LifecycleService() {
|
||||||
@@ -24,13 +23,11 @@ open class ForegroundService : LifecycleService() {
|
|||||||
when (action) {
|
when (action) {
|
||||||
Action.START.name,
|
Action.START.name,
|
||||||
Action.START_FOREGROUND.name -> startService(intent.extras)
|
Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||||
|
|
||||||
Action.STOP.name -> stopService(intent.extras)
|
Action.STOP.name -> stopService(intent.extras)
|
||||||
Constants.ALWAYS_ON_VPN_ACTION -> {
|
"android.net.VpnService" -> {
|
||||||
Timber.i("Always-on VPN starting service")
|
Timber.d("Always-on VPN starting service")
|
||||||
startService(intent.extras)
|
startService(intent.extras)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Timber.d("This should never happen. No action in the received intent")
|
else -> Timber.d("This should never happen. No action in the received intent")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -59,7 +56,7 @@ open class ForegroundService : LifecycleService() {
|
|||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.d("Service stopped without being started: ${e.message}")
|
||||||
}
|
}
|
||||||
isServiceStarted = false
|
isServiceStarted = false
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-32
@@ -3,17 +3,31 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class ServiceManager(private val appDataRepository: AppDataRepository) {
|
object ServiceManager {
|
||||||
|
|
||||||
|
// private
|
||||||
|
// fun <T> Context.isServiceRunning(service: Class<T>) =
|
||||||
|
// (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
|
||||||
|
// .runningAppProcesses.any {
|
||||||
|
// it.processName == 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(
|
private fun <T : Service> actionOnService(
|
||||||
action: Action,
|
action: Action,
|
||||||
context: Context,
|
context: Context,
|
||||||
cls: Class<T>,
|
cls: Class<T>,
|
||||||
extras: Map<String, Int>? = null
|
extras: Map<String, String>? = null
|
||||||
) {
|
) {
|
||||||
val intent =
|
val intent =
|
||||||
Intent(context, cls).also {
|
Intent(context, cls).also {
|
||||||
@@ -26,11 +40,9 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
|||||||
Action.START_FOREGROUND -> {
|
Action.START_FOREGROUND -> {
|
||||||
context.startForegroundService(intent)
|
context.startForegroundService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
Action.START -> {
|
Action.START -> {
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
Action.STOP -> context.startService(intent)
|
Action.STOP -> context.startService(intent)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -38,22 +50,16 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun startVpnService(
|
fun startVpnService(context: Context, tunnelConfig: String) {
|
||||||
context: Context,
|
|
||||||
tunnelId: Int? = null,
|
|
||||||
isManualStart: Boolean = false
|
|
||||||
) {
|
|
||||||
if (isManualStart) onManualStart(tunnelId)
|
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START,
|
Action.START,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java,
|
WireGuardTunnelService::class.java,
|
||||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
|
fun stopVpnService(context: Context) {
|
||||||
if (isManualStop) onManualStop()
|
|
||||||
Timber.d("Stopping vpn service action")
|
Timber.d("Stopping vpn service action")
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.STOP,
|
Action.STOP,
|
||||||
@@ -62,27 +68,12 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onManualStop() {
|
fun startVpnServiceForeground(context: Context, tunnelConfig: String) {
|
||||||
appDataRepository.appState.setManualStop()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun onManualStart(tunnelId: Int?) {
|
|
||||||
tunnelId?.let {
|
|
||||||
appDataRepository.appState.setTunnelRunningFromManualStart(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun startVpnServiceForeground(
|
|
||||||
context: Context,
|
|
||||||
tunnelId: Int? = null,
|
|
||||||
isManualStart: Boolean = false
|
|
||||||
) {
|
|
||||||
if (isManualStart) onManualStart(tunnelId)
|
|
||||||
actionOnService(
|
actionOnService(
|
||||||
Action.START_FOREGROUND,
|
Action.START_FOREGROUND,
|
||||||
context,
|
context,
|
||||||
WireGuardTunnelService::class.java,
|
WireGuardTunnelService::class.java,
|
||||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-84
@@ -1,84 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
|
||||||
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
|
|
||||||
data class WatcherState(
|
|
||||||
val isWifiConnected: Boolean = false,
|
|
||||||
val config: TunnelConfig? = null,
|
|
||||||
val vpnStatus: Tunnel.State = Tunnel.State.DOWN,
|
|
||||||
val isEthernetConnected: Boolean = false,
|
|
||||||
val isMobileDataConnected: Boolean = false,
|
|
||||||
val currentNetworkSSID: String = "",
|
|
||||||
val settings: Settings = Settings()
|
|
||||||
) {
|
|
||||||
|
|
||||||
private fun isVpnConnected() = vpnStatus == Tunnel.State.UP
|
|
||||||
fun isEthernetConditionMet(): Boolean {
|
|
||||||
return (isEthernetConnected &&
|
|
||||||
settings.isTunnelOnEthernetEnabled &&
|
|
||||||
!isVpnConnected())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isMobileDataConditionMet(): Boolean {
|
|
||||||
return (!isEthernetConnected &&
|
|
||||||
settings.isTunnelOnMobileDataEnabled &&
|
|
||||||
!isWifiConnected &&
|
|
||||||
isMobileDataConnected &&
|
|
||||||
!isVpnConnected())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isTunnelNotMobileDataPreferredConditionMet(): Boolean {
|
|
||||||
return (!isEthernetConnected &&
|
|
||||||
settings.isTunnelOnMobileDataEnabled &&
|
|
||||||
!isWifiConnected &&
|
|
||||||
isMobileDataConnected &&
|
|
||||||
config?.isMobileDataTunnel == false && isVpnConnected())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
|
|
||||||
return (!isEthernetConnected &&
|
|
||||||
!settings.isTunnelOnMobileDataEnabled &&
|
|
||||||
isMobileDataConnected &&
|
|
||||||
!isWifiConnected &&
|
|
||||||
isVpnConnected())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isUntrustedWifiConditionMet(): Boolean {
|
|
||||||
return (!isEthernetConnected &&
|
|
||||||
isWifiConnected &&
|
|
||||||
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
|
||||||
settings.isTunnelOnWifiEnabled
|
|
||||||
&& !isVpnConnected())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isTunnelNotWifiNamePreferredMet(ssid: String): Boolean {
|
|
||||||
return (!isEthernetConnected &&
|
|
||||||
isWifiConnected &&
|
|
||||||
!settings.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
|
|
||||||
settings.isTunnelOnWifiEnabled && config?.tunnelNetworks?.contains(ssid) == false && isVpnConnected())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isTrustedWifiConditionMet(): Boolean {
|
|
||||||
return (!isEthernetConnected &&
|
|
||||||
(isWifiConnected &&
|
|
||||||
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
|
|
||||||
(isVpnConnected()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isTunnelOffOnWifiConditionMet(): Boolean {
|
|
||||||
return (!isEthernetConnected &&
|
|
||||||
(isWifiConnected &&
|
|
||||||
!settings.isTunnelOnWifiEnabled &&
|
|
||||||
(isVpnConnected())))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isTunnelOffOnNoConnectivityMet(): Boolean {
|
|
||||||
return (!isEthernetConnected &&
|
|
||||||
!isWifiConnected &&
|
|
||||||
!isMobileDataConnected &&
|
|
||||||
(isVpnConnected()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+140
-171
@@ -1,14 +1,18 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||||
|
|
||||||
|
import android.app.AlarmManager
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import android.os.SystemClock
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||||
@@ -24,38 +28,37 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.InetAddress
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
private val foregroundId = 122
|
private val foregroundId = 122
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var wifiService: NetworkService<WifiService>
|
||||||
lateinit var wifiService: NetworkService<WifiService>
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var mobileDataService: NetworkService<MobileDataService>
|
||||||
lateinit var mobileDataService: NetworkService<MobileDataService>
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var ethernetService: NetworkService<EthernetService>
|
||||||
lateinit var ethernetService: NetworkService<EthernetService>
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var appDataRepository: AppDataRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var notificationService: NotificationService
|
||||||
lateinit var notificationService: NotificationService
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var vpnService: VpnService
|
||||||
lateinit var vpnService: VpnService
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var serviceManager: ServiceManager
|
|
||||||
|
|
||||||
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
private val networkEventsFlow = MutableStateFlow(WatcherState())
|
||||||
|
|
||||||
|
data class WatcherState(
|
||||||
|
val isWifiConnected: Boolean = false,
|
||||||
|
val isVpnConnected: Boolean = false,
|
||||||
|
val isEthernetConnected: Boolean = false,
|
||||||
|
val isMobileDataConnected: Boolean = false,
|
||||||
|
val currentNetworkSSID: String = "",
|
||||||
|
val settings: Settings = Settings()
|
||||||
|
)
|
||||||
|
|
||||||
private lateinit var watcherJob: Job
|
private lateinit var watcherJob: Job
|
||||||
|
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
@@ -65,7 +68,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
if (settingsRepository.getSettings().isAutoTunnelPaused) {
|
||||||
launchWatcherPausedNotification()
|
launchWatcherPausedNotification()
|
||||||
} else launchWatcherNotification()
|
} else launchWatcherNotification()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -119,15 +122,42 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initWakeLock() {
|
// TODO could this be restarting service in a bad state?
|
||||||
|
// 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 suspend fun initWakeLock() {
|
||||||
|
val isBatterySaverOn =
|
||||||
|
withContext(lifecycleScope.coroutineContext) {
|
||||||
|
settingsRepository.getSettings().isBatterySaverEnabled
|
||||||
|
}
|
||||||
wakeLock =
|
wakeLock =
|
||||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||||
try {
|
if (isBatterySaverOn) {
|
||||||
Timber.i("Initiating wakelock with 10 min timeout")
|
Timber.d("Initiating wakelock with timeout")
|
||||||
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||||
} finally {
|
} else {
|
||||||
release()
|
Timber.d("Initiating wakelock with zero timeout")
|
||||||
|
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,42 +172,35 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
private fun startWatcherJob() {
|
private fun startWatcherJob() {
|
||||||
watcherJob =
|
watcherJob =
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val setting = appDataRepository.settings.getSettings()
|
val setting = settingsRepository.getSettings()
|
||||||
launch {
|
launch {
|
||||||
Timber.i("Starting wifi watcher")
|
Timber.d("Starting wifi watcher")
|
||||||
watchForWifiConnectivityChanges()
|
watchForWifiConnectivityChanges()
|
||||||
}
|
}
|
||||||
if (setting.isTunnelOnMobileDataEnabled) {
|
if (setting.isTunnelOnMobileDataEnabled) {
|
||||||
launch {
|
launch {
|
||||||
Timber.i("Starting mobile data watcher")
|
Timber.d("Starting mobile data watcher")
|
||||||
watchForMobileDataConnectivityChanges()
|
watchForMobileDataConnectivityChanges()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (setting.isTunnelOnEthernetEnabled) {
|
if (setting.isTunnelOnEthernetEnabled) {
|
||||||
launch {
|
launch {
|
||||||
Timber.i("Starting ethernet data watcher")
|
Timber.d("Starting ethernet data watcher")
|
||||||
watchForEthernetConnectivityChanges()
|
watchForEthernetConnectivityChanges()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
Timber.i("Starting vpn state watcher")
|
Timber.d("Starting vpn state watcher")
|
||||||
watchForVpnConnectivityChanges()
|
watchForVpnConnectivityChanges()
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
Timber.i("Starting settings watcher")
|
Timber.d("Starting settings watcher")
|
||||||
watchForSettingsChanges()
|
watchForSettingsChanges()
|
||||||
}
|
}
|
||||||
if (setting.isPingEnabled) {
|
|
||||||
launch {
|
|
||||||
Timber.i("Starting ping watcher")
|
|
||||||
watchForPingFailure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launch {
|
launch {
|
||||||
Timber.i("Starting management watcher")
|
Timber.d("Starting management watcher")
|
||||||
manageVpn()
|
manageVpn()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,68 +208,32 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
mobileDataService.networkStatus.collect {
|
mobileDataService.networkStatus.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.i("Gained Mobile data connection")
|
Timber.d("Gained Mobile data connection")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isMobileDataConnected = true,
|
isMobileDataConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isMobileDataConnected = true,
|
isMobileDataConnected = true,
|
||||||
)
|
)
|
||||||
Timber.i("Mobile data capabilities changed")
|
Timber.d("Mobile data capabilities changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isMobileDataConnected = false,
|
isMobileDataConnected = false,
|
||||||
)
|
)
|
||||||
Timber.i("Lost mobile data connection")
|
Timber.d("Lost mobile data connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun watchForPingFailure() {
|
|
||||||
try {
|
|
||||||
do {
|
|
||||||
if (vpnService.vpnState.value.status == Tunnel.State.UP) {
|
|
||||||
val tunnelConfig = vpnService.vpnState.value.tunnelConfig
|
|
||||||
tunnelConfig?.let {
|
|
||||||
val config = TunnelConfig.configFromQuick(it.wgQuick)
|
|
||||||
val results = config.peers.map { peer ->
|
|
||||||
val host = if (peer.endpoint.isPresent &&
|
|
||||||
peer.endpoint.get().resolved.isPresent)
|
|
||||||
peer.endpoint.get().resolved.get().host
|
|
||||||
else Constants.BACKUP_PING_HOST
|
|
||||||
Timber.i("Checking reachability of: $host")
|
|
||||||
val reachable = InetAddress.getByName(host)
|
|
||||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
|
||||||
Timber.i("Result: reachable - $reachable")
|
|
||||||
reachable
|
|
||||||
}
|
|
||||||
if (results.contains(false)) {
|
|
||||||
Timber.i("Restarting VPN for ping failure")
|
|
||||||
serviceManager.stopVpnService(this)
|
|
||||||
delay(Constants.VPN_RESTART_DELAY)
|
|
||||||
serviceManager.startVpnServiceForeground(this)
|
|
||||||
delay(Constants.PING_COOLDOWN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delay(Constants.PING_INTERVAL)
|
|
||||||
} while (true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun watchForSettingsChanges() {
|
private suspend fun watchForSettingsChanges() {
|
||||||
appDataRepository.settings.getSettingsFlow().collect {
|
settingsRepository.getSettingsFlow().collect {
|
||||||
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||||
when (it.isAutoTunnelPaused) {
|
when (it.isAutoTunnelPaused) {
|
||||||
true -> launchWatcherPausedNotification()
|
true -> launchWatcherPausedNotification()
|
||||||
@@ -262,11 +249,19 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
|
|
||||||
private suspend fun watchForVpnConnectivityChanges() {
|
private suspend fun watchForVpnConnectivityChanges() {
|
||||||
vpnService.vpnState.collect {
|
vpnService.vpnState.collect {
|
||||||
networkEventsFlow.value =
|
when (it.status) {
|
||||||
networkEventsFlow.value.copy(
|
Tunnel.State.DOWN ->
|
||||||
vpnStatus = it.status,
|
networkEventsFlow.value =
|
||||||
config = it.tunnelConfig,
|
networkEventsFlow.value.copy(
|
||||||
)
|
isVpnConnected = false,
|
||||||
|
)
|
||||||
|
Tunnel.State.UP ->
|
||||||
|
networkEventsFlow.value =
|
||||||
|
networkEventsFlow.value.copy(
|
||||||
|
isVpnConnected = true,
|
||||||
|
)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,27 +269,25 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
ethernetService.networkStatus.collect {
|
ethernetService.networkStatus.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.i("Gained Ethernet connection")
|
Timber.d("Gained Ethernet connection")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isEthernetConnected = true,
|
isEthernetConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.i("Ethernet capabilities changed")
|
Timber.d("Ethernet capabilities changed")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isEthernetConnected = true,
|
isEthernetConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isEthernetConnected = false,
|
isEthernetConnected = false,
|
||||||
)
|
)
|
||||||
Timber.i("Lost Ethernet connection")
|
Timber.d("Lost Ethernet connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,119 +297,95 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
wifiService.networkStatus.collect {
|
wifiService.networkStatus.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.i("Gained Wi-Fi connection")
|
Timber.d("Gained Wi-Fi connection")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isWifiConnected = true,
|
isWifiConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.i("Wifi capabilities changed")
|
Timber.d("Wifi capabilities changed")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isWifiConnected = true,
|
isWifiConnected = true,
|
||||||
)
|
)
|
||||||
val ssid = wifiService.getNetworkName(it.networkCapabilities)
|
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||||
ssid?.let {
|
Timber.d("Detected SSID: $ssid")
|
||||||
Timber.i("Detected SSID: $ssid")
|
networkEventsFlow.value =
|
||||||
appDataRepository.appState.setCurrentSsid(ssid)
|
networkEventsFlow.value.copy(
|
||||||
networkEventsFlow.value =
|
currentNetworkSSID = ssid,
|
||||||
networkEventsFlow.value.copy(
|
)
|
||||||
currentNetworkSSID = ssid,
|
|
||||||
)
|
|
||||||
} ?: Timber.w("Failed to read ssid")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isWifiConnected = false,
|
isWifiConnected = false,
|
||||||
)
|
)
|
||||||
Timber.i("Lost Wi-Fi connection")
|
Timber.d("Lost Wi-Fi connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getMobileDataTunnel(): TunnelConfig? {
|
// TODO clean this up
|
||||||
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getSsidTunnel(ssid: String): TunnelConfig? {
|
|
||||||
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun manageVpn() {
|
private suspend fun manageVpn() {
|
||||||
networkEventsFlow.collectLatest { watcherState ->
|
networkEventsFlow.collectLatest {
|
||||||
val autoTunnel = "Auto-tunnel watcher"
|
Timber.i("New watcher state: $it")
|
||||||
if (!watcherState.settings.isAutoTunnelPaused) {
|
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
|
||||||
//delay for rapid network state changes and then collect latest
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
delay(Constants.WATCHER_COLLECTION_DELAY)
|
|
||||||
when {
|
when {
|
||||||
watcherState.isEthernetConditionMet() -> {
|
((it.isEthernetConnected &&
|
||||||
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
it.settings.isTunnelOnEthernetEnabled &&
|
||||||
serviceManager.startVpnServiceForeground(this)
|
!it.isVpnConnected)) -> {
|
||||||
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
|
Timber.i("Condition 1 met")
|
||||||
}
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
watcherState.isMobileDataConditionMet() -> {
|
it.settings.isTunnelOnMobileDataEnabled &&
|
||||||
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
|
!it.isWifiConnected &&
|
||||||
serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
|
it.isMobileDataConnected &&
|
||||||
|
!it.isVpnConnected) -> {
|
||||||
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
|
Timber.i("Condition 2 met")
|
||||||
}
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
watcherState.isTunnelNotMobileDataPreferredConditionMet() -> {
|
!it.settings.isTunnelOnMobileDataEnabled &&
|
||||||
getMobileDataTunnel()?.let {
|
!it.isWifiConnected &&
|
||||||
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
|
it.isVpnConnected) -> {
|
||||||
serviceManager.startVpnServiceForeground(
|
ServiceManager.stopVpnService(this)
|
||||||
this,
|
Timber.i("Condition 3 met")
|
||||||
getMobileDataTunnel()?.id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
it.isWifiConnected &&
|
||||||
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
!it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) &&
|
||||||
serviceManager.stopVpnService(this)
|
it.settings.isTunnelOnWifiEnabled &&
|
||||||
|
(!it.isVpnConnected)) -> {
|
||||||
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
|
Timber.i("Condition 4 met")
|
||||||
}
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> {
|
(it.isWifiConnected &&
|
||||||
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
|
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
||||||
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
|
(it.isVpnConnected)) -> {
|
||||||
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
|
ServiceManager.stopVpnService(this)
|
||||||
serviceManager.startVpnServiceForeground(this, it.id)
|
Timber.i("Condition 5 met")
|
||||||
} ?: suspend {
|
|
||||||
Timber.i("No tunnel associated with this SSID, using defaults")
|
|
||||||
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) {
|
|
||||||
serviceManager.startVpnServiceForeground(this)
|
|
||||||
}
|
|
||||||
}.invoke()
|
|
||||||
}
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
watcherState.isUntrustedWifiConditionMet() -> {
|
(it.isWifiConnected &&
|
||||||
Timber.i("$autoTunnel - tunnel on untrusted wifi condition met")
|
!it.settings.isTunnelOnWifiEnabled &&
|
||||||
serviceManager.startVpnServiceForeground(
|
(it.isVpnConnected))) -> {
|
||||||
this,
|
ServiceManager.stopVpnService(this)
|
||||||
getSsidTunnel(watcherState.currentNetworkSSID)?.id,
|
Timber.i("Condition 6 met")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
(!it.isEthernetConnected &&
|
||||||
watcherState.isTrustedWifiConditionMet() -> {
|
!it.isWifiConnected &&
|
||||||
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
!it.isMobileDataConnected &&
|
||||||
serviceManager.stopVpnService(this)
|
(it.isVpnConnected)) -> {
|
||||||
|
ServiceManager.stopVpnService(this)
|
||||||
|
Timber.i("Condition 7 met")
|
||||||
}
|
}
|
||||||
|
|
||||||
watcherState.isTunnelOffOnWifiConditionMet() -> {
|
|
||||||
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
|
|
||||||
serviceManager.stopVpnService(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
watcherState.isTunnelOffOnNoConnectivityMet() -> {
|
|
||||||
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
|
|
||||||
serviceManager.stopVpnService(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Timber.i("$autoTunnel - no condition met")
|
Timber.i("No condition met")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-71
@@ -5,9 +5,10 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.wireguard.android.backend.Tunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
|
||||||
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
@@ -20,30 +21,30 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardTunnelService : ForegroundService() {
|
class WireGuardTunnelService : ForegroundService() {
|
||||||
private val foregroundId = 123
|
private val foregroundId = 123
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var vpnService: VpnService
|
||||||
lateinit var vpnService: VpnService
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var appDataRepository: AppDataRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var notificationService: NotificationService
|
|
||||||
|
@Inject lateinit var notificationService: NotificationService
|
||||||
|
|
||||||
private lateinit var job: Job
|
private lateinit var job: Job
|
||||||
|
|
||||||
|
private var tunnelName: String = ""
|
||||||
private var didShowConnected = false
|
private var didShowConnected = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
//TODO fix this to not launch if AOVPN
|
if (tunnelConfigRepository.getAll().isNotEmpty()) {
|
||||||
if (appDataRepository.tunnels.count() != 0) {
|
|
||||||
launchVpnNotification()
|
launchVpnNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,69 +53,67 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
override fun startService(extras: Bundle?) {
|
override fun startService(extras: Bundle?) {
|
||||||
super.startService(extras)
|
super.startService(extras)
|
||||||
cancelJob()
|
cancelJob()
|
||||||
|
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||||
|
val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) }
|
||||||
|
tunnelName = tunnelConfig?.name ?: ""
|
||||||
job =
|
job =
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
launch {
|
launch {
|
||||||
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
|
if (tunnelConfig != null) {
|
||||||
if (vpnService.getState() == Tunnel.State.UP) {
|
try {
|
||||||
vpnService.stopTunnel()
|
tunnelName = tunnelConfig.name
|
||||||
}
|
vpnService.startTunnel(tunnelConfig)
|
||||||
vpnService.startTunnel(
|
} catch (e: Exception) {
|
||||||
tunnelId?.let {
|
Timber.e("Problem starting tunnel: ${e.message}")
|
||||||
appDataRepository.tunnels.getById(it)
|
stopService(extras)
|
||||||
},
|
}
|
||||||
)
|
} else {
|
||||||
}
|
Timber.d("Tunnel config null, starting default tunnel or first")
|
||||||
launch {
|
val settings = settingsRepository.getSettings()
|
||||||
handshakeNotifications()
|
val tunnels = tunnelConfigRepository.getAll()
|
||||||
}
|
if (settings.isAlwaysOnVpnEnabled) {
|
||||||
}
|
val tunnel =
|
||||||
}
|
if (settings.defaultTunnel != null) {
|
||||||
|
TunnelConfig.from(settings.defaultTunnel!!)
|
||||||
//TODO improve tunnel notifications
|
} else if (tunnels.isNotEmpty()) {
|
||||||
private suspend fun handshakeNotifications() {
|
tunnels.first()
|
||||||
var tunnelName: String? = null
|
} else {
|
||||||
vpnService.vpnState.collect { state ->
|
null
|
||||||
state.statistics
|
}
|
||||||
?.mapPeerStats()
|
if (tunnel != null) {
|
||||||
?.map { it.value?.handshakeStatus() }
|
tunnelName = tunnel.name
|
||||||
.let { statuses ->
|
vpnService.startTunnel(tunnel)
|
||||||
when {
|
|
||||||
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
|
||||||
if (!didShowConnected) {
|
|
||||||
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
|
||||||
tunnelName = state.tunnelConfig?.name
|
|
||||||
launchVpnNotification(
|
|
||||||
getString(R.string.tunnel_start_title),
|
|
||||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
|
||||||
)
|
|
||||||
didShowConnected = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
|
||||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
|
||||||
true -> {
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (state.status == Tunnel.State.UP && state.tunnelConfig?.name != tunnelName) {
|
// TODO add failed to connect notification
|
||||||
tunnelName = state.tunnelConfig?.name
|
launch {
|
||||||
launchVpnNotification(
|
vpnService.vpnState.collect { state ->
|
||||||
getString(R.string.tunnel_start_title),
|
state.statistics
|
||||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
?.mapPeerStats()
|
||||||
)
|
?.map { it.value?.handshakeStatus() }
|
||||||
|
.let { statuses ->
|
||||||
|
when {
|
||||||
|
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> {
|
||||||
|
if (!didShowConnected) {
|
||||||
|
delay(Constants.VPN_CONNECTED_NOTIFICATION_DELAY)
|
||||||
|
launchVpnNotification(
|
||||||
|
getString(R.string.tunnel_start_title),
|
||||||
|
"${getString(R.string.tunnel_start_text)} $tunnelName",
|
||||||
|
)
|
||||||
|
didShowConnected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||||
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||||
|
true -> {}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchAlwaysOnDisabledNotification() {
|
|
||||||
launchVpnNotification(
|
|
||||||
title = this.getString(R.string.vpn_connection_failed),
|
|
||||||
description = this.getString(R.string.always_on_disabled),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopService(extras: Bundle?) {
|
override fun stopService(extras: Bundle?) {
|
||||||
@@ -155,12 +154,12 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
channelId = getString(R.string.vpn_channel_id),
|
channelId = getString(R.string.vpn_channel_id),
|
||||||
channelName = getString(R.string.vpn_channel_name),
|
channelName = getString(R.string.vpn_channel_name),
|
||||||
action =
|
action =
|
||||||
PendingIntent.getBroadcast(
|
PendingIntent.getBroadcast(
|
||||||
this,
|
this,
|
||||||
0,
|
0,
|
||||||
Intent(this, NotificationActionReceiver::class.java),
|
Intent(this, NotificationActionReceiver::class.java),
|
||||||
PendingIntent.FLAG_IMMUTABLE,
|
PendingIntent.FLAG_IMMUTABLE,
|
||||||
),
|
),
|
||||||
actionText = getString(R.string.restart),
|
actionText = getString(R.string.restart),
|
||||||
title = getString(R.string.vpn_connection_failed),
|
title = getString(R.string.vpn_connection_failed),
|
||||||
onGoing = false,
|
onGoing = false,
|
||||||
|
|||||||
+1
-2
@@ -53,7 +53,6 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
object : ConnectivityManager.NetworkCallback() {
|
object : ConnectivityManager.NetworkCallback() {
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
@@ -118,7 +117,7 @@ inline fun <Result> Flow<NetworkStatus>.map(
|
|||||||
crossinline onUnavailable: suspend (network: Network) -> Result,
|
crossinline onUnavailable: suspend (network: Network) -> Result,
|
||||||
crossinline onAvailable: suspend (network: Network) -> Result,
|
crossinline onAvailable: suspend (network: Network) -> Result,
|
||||||
crossinline onCapabilitiesChanged:
|
crossinline onCapabilitiesChanged:
|
||||||
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
|
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
|
||||||
): Flow<Result> = map { status ->
|
): Flow<Result> = map { status ->
|
||||||
when (status) {
|
when (status) {
|
||||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
||||||
|
|||||||
+5
-5
@@ -45,10 +45,10 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
|
|||||||
): Notification {
|
): Notification {
|
||||||
val channel =
|
val channel =
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
channelId,
|
channelId,
|
||||||
channelName,
|
channelName,
|
||||||
importance,
|
importance,
|
||||||
)
|
)
|
||||||
.let {
|
.let {
|
||||||
it.description = title
|
it.description = title
|
||||||
it.enableLights(lights)
|
it.enableLights(lights)
|
||||||
@@ -94,7 +94,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
|
|||||||
.setOngoing(onGoing)
|
.setOngoing(onGoing)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setShowWhen(showTimestamp)
|
.setShowWhen(showTimestamp)
|
||||||
.setSmallIcon(R.drawable.ic_launcher)
|
.setSmallIcon(R.mipmap.ic_launcher_foreground)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-39
@@ -1,66 +1,80 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ShortcutsActivity : ComponentActivity() {
|
class ShortcutsActivity : ComponentActivity() {
|
||||||
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var appDataRepository: AppDataRepository
|
|
||||||
|
|
||||||
@Inject
|
private suspend fun toggleWatcherServicePause() {
|
||||||
lateinit var serviceManager: ServiceManager
|
val settings = settingsRepository.getSettings()
|
||||||
|
if (settings.isAutoTunnelEnabled) {
|
||||||
|
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||||
|
settingsRepository.save(
|
||||||
|
settings.copy(
|
||||||
|
isAutoTunnelPaused = pauseAutoTunnel,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
setContentView(View(this))
|
||||||
val settings = appDataRepository.settings.getSettings()
|
if (
|
||||||
if (settings.isShortcutsEnabled) {
|
intent
|
||||||
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
||||||
WireGuardTunnelService::class.java.simpleName -> {
|
.equals(WireGuardTunnelService::class.java.simpleName)
|
||||||
|
) {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
val settings = settingsRepository.getSettings()
|
||||||
|
if (settings.isShortcutsEnabled) {
|
||||||
|
try {
|
||||||
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
val tunnelName = intent.getStringExtra(TUNNEL_NAME_EXTRA_KEY)
|
||||||
val tunnelConfig = tunnelName?.let {
|
val tunnelConfig =
|
||||||
appDataRepository.tunnels.getAll().firstOrNull {
|
if (tunnelName != null) {
|
||||||
it.name == tunnelName
|
tunnelConfigRepository.getAll().firstOrNull {
|
||||||
|
it.name == tunnelName
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (settings.defaultTunnel == null) {
|
||||||
|
tunnelConfigRepository.getAll().first()
|
||||||
|
} else {
|
||||||
|
TunnelConfig.from(settings.defaultTunnel!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
tunnelConfig ?: return@launch
|
||||||
|
toggleWatcherServicePause()
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Action.START.name -> serviceManager.startVpnServiceForeground(
|
Action.STOP.name ->
|
||||||
this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true,
|
ServiceManager.stopVpnService(
|
||||||
)
|
this@ShortcutsActivity,
|
||||||
|
)
|
||||||
Action.STOP.name -> serviceManager.stopVpnService(
|
Action.START.name ->
|
||||||
this@ShortcutsActivity,
|
ServiceManager.startVpnServiceForeground(
|
||||||
isManualStop = true,
|
this@ShortcutsActivity,
|
||||||
)
|
tunnelConfig.toString(),
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
|
||||||
WireGuardConnectivityWatcherService::class.java.simpleName -> {
|
|
||||||
when (intent.action) {
|
|
||||||
Action.START.name -> appDataRepository.settings.save(
|
|
||||||
settings.copy(
|
|
||||||
isAutoTunnelPaused = false,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
Action.STOP.name -> appDataRepository.settings.save(
|
|
||||||
settings.copy(
|
|
||||||
isAutoTunnelPaused = true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e.message)
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-106
@@ -1,106 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.tile
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.service.quicksettings.Tile
|
|
||||||
import android.service.quicksettings.TileService
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class AutoTunnelControlTile : TileService() {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var appDataRepository: AppDataRepository
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var serviceManager: ServiceManager
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
|
||||||
|
|
||||||
private var manualStartConfig: TunnelConfig? = null
|
|
||||||
|
|
||||||
override fun onStartListening() {
|
|
||||||
super.onStartListening()
|
|
||||||
scope.launch {
|
|
||||||
appDataRepository.settings.getSettingsFlow().collectLatest {
|
|
||||||
when (it.isAutoTunnelEnabled) {
|
|
||||||
true -> {
|
|
||||||
if (it.isAutoTunnelPaused) {
|
|
||||||
setInactive()
|
|
||||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.paused))
|
|
||||||
} else {
|
|
||||||
setActive()
|
|
||||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.active))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false -> {
|
|
||||||
setTileDescription(this@AutoTunnelControlTile.getString(R.string.disabled))
|
|
||||||
setUnavailable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTileRemoved() {
|
|
||||||
super.onTileRemoved()
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick() {
|
|
||||||
super.onClick()
|
|
||||||
unlockAndRun {
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
appDataRepository.toggleWatcherServicePause()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e.message)
|
|
||||||
} finally {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setActive() {
|
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
|
||||||
qsTile.updateTile()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInactive() {
|
|
||||||
qsTile.state = Tile.STATE_INACTIVE
|
|
||||||
qsTile.updateTile()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setUnavailable() {
|
|
||||||
manualStartConfig = null
|
|
||||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
|
||||||
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
|
|
||||||
}
|
|
||||||
qsTile.updateTile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+44
-29
@@ -5,7 +5,8 @@ import android.service.quicksettings.Tile
|
|||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
@@ -17,44 +18,41 @@ import timber.log.Timber
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TunnelControlTile : TileService() {
|
class TunnelControlTile() : TileService() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
lateinit var appDataRepository: AppDataRepository
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var vpnService: VpnService
|
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var vpnService: VpnService
|
||||||
lateinit var serviceManager: ServiceManager
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
private var manualStartConfig: TunnelConfig? = null
|
private var tunnelName: String? = null
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
Timber.d("On start listening called")
|
Timber.d("On start listening called")
|
||||||
scope.launch {
|
scope.launch {
|
||||||
vpnService.vpnState.collect { it ->
|
vpnService.vpnState.collect {
|
||||||
when (it.status) {
|
when (it.status) {
|
||||||
Tunnel.State.UP -> {
|
Tunnel.State.UP -> setActive()
|
||||||
setActive()
|
Tunnel.State.DOWN -> setInactive()
|
||||||
it.tunnelConfig?.name?.let { name -> setTileDescription(name) }
|
|
||||||
}
|
|
||||||
|
|
||||||
Tunnel.State.DOWN -> {
|
|
||||||
setInactive()
|
|
||||||
val config = appDataRepository.getStartTunnelConfig()?.also { config ->
|
|
||||||
manualStartConfig = config
|
|
||||||
} ?: appDataRepository.getPrimaryOrFirstTunnel()
|
|
||||||
config?.let {
|
|
||||||
setTileDescription(it.name)
|
|
||||||
} ?: setUnavailable()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> setInactive()
|
else -> setInactive()
|
||||||
}
|
}
|
||||||
|
val tunnels = tunnelConfigRepository.getAll()
|
||||||
|
if (tunnels.isEmpty()) {
|
||||||
|
setUnavailable()
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
tunnelName =
|
||||||
|
it.name.ifBlank {
|
||||||
|
val settings = settingsRepository.getSettings()
|
||||||
|
if (settings.defaultTunnel != null) {
|
||||||
|
TunnelConfig.from(settings.defaultTunnel!!).name
|
||||||
|
} else tunnels.firstOrNull()?.name
|
||||||
|
}
|
||||||
|
setTileDescription(tunnelName ?: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,11 +72,15 @@ class TunnelControlTile : TileService() {
|
|||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
|
val tunnelConfig =
|
||||||
|
tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
||||||
|
toggleWatcherServicePause()
|
||||||
if (vpnService.getState() == Tunnel.State.UP) {
|
if (vpnService.getState() == Tunnel.State.UP) {
|
||||||
serviceManager.stopVpnService(this@TunnelControlTile, isManualStop = true)
|
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||||
} else {
|
} else {
|
||||||
serviceManager.startVpnServiceForeground(
|
ServiceManager.startVpnServiceForeground(
|
||||||
this@TunnelControlTile, manualStartConfig?.id, isManualStart = true,
|
this@TunnelControlTile,
|
||||||
|
tunnelConfig.toString(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -90,6 +92,20 @@ class TunnelControlTile : TileService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun toggleWatcherServicePause() {
|
||||||
|
scope.launch {
|
||||||
|
val settings = settingsRepository.getSettings()
|
||||||
|
if (settings.isAutoTunnelEnabled) {
|
||||||
|
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||||
|
settingsRepository.save(
|
||||||
|
settings.copy(
|
||||||
|
isAutoTunnelPaused = pauseAutoTunnel,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setActive() {
|
private fun setActive() {
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
qsTile.state = Tile.STATE_ACTIVE
|
||||||
qsTile.updateTile()
|
qsTile.updateTile()
|
||||||
@@ -101,7 +117,6 @@ class TunnelControlTile : TileService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setUnavailable() {
|
private fun setUnavailable() {
|
||||||
manualStartConfig = null
|
|
||||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||||
qsTile.updateTile()
|
qsTile.updateTile()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface VpnService : Tunnel {
|
interface VpnService : Tunnel {
|
||||||
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State
|
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
||||||
|
|
||||||
suspend fun stopTunnel()
|
suspend fun stopTunnel()
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
|||||||
|
|
||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
|
|
||||||
data class VpnState(
|
data class VpnState(
|
||||||
val status: Tunnel.State = Tunnel.State.DOWN,
|
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||||
val tunnelConfig: TunnelConfig? = null,
|
val name: String = "",
|
||||||
val statistics: Statistics? = null
|
val statistics: Statistics? = null
|
||||||
)
|
)
|
||||||
|
|||||||
+32
-25
@@ -4,9 +4,10 @@ import com.wireguard.android.backend.Backend
|
|||||||
import com.wireguard.android.backend.BackendException
|
import com.wireguard.android.backend.BackendException
|
||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
import com.wireguard.android.backend.Tunnel.State
|
import com.wireguard.android.backend.Tunnel.State
|
||||||
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
import com.zaneschepke.wireguardautotunnel.module.Kernel
|
||||||
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
import com.zaneschepke.wireguardautotunnel.module.Userspace
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
@@ -26,7 +27,7 @@ class WireGuardTunnel
|
|||||||
constructor(
|
constructor(
|
||||||
@Userspace private val userspaceBackend: Backend,
|
@Userspace private val userspaceBackend: Backend,
|
||||||
@Kernel private val kernelBackend: Backend,
|
@Kernel private val kernelBackend: Backend,
|
||||||
private val appDataRepository: AppDataRepository,
|
private val settingsRepository: SettingsRepository
|
||||||
) : VpnService {
|
) : VpnService {
|
||||||
private val _vpnState = MutableStateFlow(VpnState())
|
private val _vpnState = MutableStateFlow(VpnState())
|
||||||
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||||
@@ -35,13 +36,15 @@ constructor(
|
|||||||
|
|
||||||
private lateinit var statsJob: Job
|
private lateinit var statsJob: Job
|
||||||
|
|
||||||
|
private var config: Config? = null
|
||||||
|
|
||||||
private var backend: Backend = userspaceBackend
|
private var backend: Backend = userspaceBackend
|
||||||
|
|
||||||
private var backendIsUserspace = true
|
private var backendIsUserspace = true
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
appDataRepository.settings.getSettingsFlow().collect {
|
settingsRepository.getSettingsFlow().collect {
|
||||||
if (it.isKernelEnabled && backendIsUserspace) {
|
if (it.isKernelEnabled && backendIsUserspace) {
|
||||||
Timber.d("Setting kernel backend")
|
Timber.d("Setting kernel backend")
|
||||||
backend = kernelBackend
|
backend = kernelBackend
|
||||||
@@ -55,22 +58,20 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): State {
|
override suspend fun startTunnel(tunnelConfig: TunnelConfig): State {
|
||||||
return try {
|
return try {
|
||||||
//TODO we need better error handling here
|
stopTunnelOnConfigChange(tunnelConfig)
|
||||||
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
emitTunnelName(tunnelConfig.name)
|
||||||
if (config != null) {
|
config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
emitTunnelConfig(config)
|
val state =
|
||||||
val wgConfig = TunnelConfig.configFromQuick(config.wgQuick)
|
backend.setState(
|
||||||
val state =
|
this,
|
||||||
backend.setState(
|
State.UP,
|
||||||
this,
|
config,
|
||||||
State.UP,
|
)
|
||||||
wgConfig,
|
emitTunnelState(state)
|
||||||
)
|
state
|
||||||
state
|
} catch (e: Exception) {
|
||||||
} else throw Exception("No tunnels")
|
|
||||||
} catch (e: BackendException) {
|
|
||||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||||
State.DOWN
|
State.DOWN
|
||||||
}
|
}
|
||||||
@@ -92,14 +93,24 @@ constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
|
private suspend fun emitTunnelName(name: String) {
|
||||||
_vpnState.emit(
|
_vpnState.emit(
|
||||||
_vpnState.value.copy(
|
_vpnState.value.copy(
|
||||||
tunnelConfig = tunnelConfig,
|
name = name,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
||||||
|
if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
|
||||||
|
stopTunnel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return _vpnState.value.name
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun stopTunnel() {
|
override suspend fun stopTunnel() {
|
||||||
try {
|
try {
|
||||||
if (getState() == State.UP) {
|
if (getState() == State.UP) {
|
||||||
@@ -115,14 +126,10 @@ constructor(
|
|||||||
return backend.getState(this)
|
return backend.getState(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
|
||||||
return _vpnState.value.tunnelConfig?.name ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStateChange(state: State) {
|
override fun onStateChange(state: State) {
|
||||||
val tunnel = this
|
val tunnel = this
|
||||||
emitTunnelState(state)
|
emitTunnelState(state)
|
||||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
if (state == State.UP) {
|
if (state == State.UP) {
|
||||||
statsJob =
|
statsJob =
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.SettingsDao
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ActivityViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val settingsRepo: SettingsDao,
|
||||||
|
) : ViewModel() {}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui
|
|
||||||
|
|
||||||
data class AppUiState(
|
|
||||||
val snackbarMessage: String = "",
|
|
||||||
val snackbarMessageConsumed: Boolean = true,
|
|
||||||
val vpnPermissionAccepted: Boolean = false,
|
|
||||||
val notificationPermissionAccepted: Boolean = false,
|
|
||||||
val requestPermissions: Boolean = false
|
|
||||||
)
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.wireguard.android.backend.GoBackend
|
|
||||||
import com.zaneschepke.logcatter.Logcatter
|
|
||||||
import com.zaneschepke.logcatter.model.LogMessage
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.time.Instant
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class AppViewModel
|
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
private val application: Application,
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance)
|
|
||||||
|
|
||||||
private val _appUiState = MutableStateFlow(
|
|
||||||
AppUiState(
|
|
||||||
vpnPermissionAccepted = vpnIntent == null,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
val appUiState = _appUiState.asStateFlow()
|
|
||||||
|
|
||||||
|
|
||||||
fun isRequiredPermissionGranted(): Boolean {
|
|
||||||
val allAccepted =
|
|
||||||
(_appUiState.value.vpnPermissionAccepted && _appUiState.value.vpnPermissionAccepted)
|
|
||||||
if (!allAccepted) requestPermissions()
|
|
||||||
return allAccepted
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requestPermissions() {
|
|
||||||
_appUiState.value = _appUiState.value.copy(
|
|
||||||
requestPermissions = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun permissionsRequested() {
|
|
||||||
_appUiState.value = _appUiState.value.copy(
|
|
||||||
requestPermissions = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openWebPage(url: String) {
|
|
||||||
try {
|
|
||||||
val webpage: Uri = Uri.parse(url)
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, webpage).apply {
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
}
|
|
||||||
application.startActivity(intent)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
Timber.e(e)
|
|
||||||
showSnackbarMessage(application.getString(R.string.no_browser_detected))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onVpnPermissionAccepted() {
|
|
||||||
_appUiState.value = _appUiState.value.copy(
|
|
||||||
vpnPermissionAccepted = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchEmail() {
|
|
||||||
try {
|
|
||||||
val intent =
|
|
||||||
Intent(Intent.ACTION_SENDTO).apply {
|
|
||||||
type = Constants.EMAIL_MIME_TYPE
|
|
||||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(application.getString(R.string.my_email)))
|
|
||||||
putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject))
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
}
|
|
||||||
application.startActivity(
|
|
||||||
Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply {
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
Timber.e(e)
|
|
||||||
showSnackbarMessage(application.getString(R.string.no_email_detected))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showSnackbarMessage(message: String) {
|
|
||||||
_appUiState.value = _appUiState.value.copy(
|
|
||||||
snackbarMessage = message,
|
|
||||||
snackbarMessageConsumed = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun snackbarMessageConsumed() {
|
|
||||||
_appUiState.value = _appUiState.value.copy(
|
|
||||||
snackbarMessage = "",
|
|
||||||
snackbarMessageConsumed = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val logs = mutableStateListOf<LogMessage>()
|
|
||||||
|
|
||||||
fun readLogCatOutput() =
|
|
||||||
viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
|
|
||||||
launch {
|
|
||||||
Logcatter.logs {
|
|
||||||
logs.add(it)
|
|
||||||
if (logs.size > Constants.LOG_BUFFER_SIZE) {
|
|
||||||
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearLogs() {
|
|
||||||
logs.clear()
|
|
||||||
Logcatter.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveLogsToFile() {
|
|
||||||
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
|
|
||||||
val content = logs.joinToString(separator = "\n")
|
|
||||||
FileUtils.saveFileToDownloads(application.applicationContext, content, fileName)
|
|
||||||
Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setNotificationPermissionAccepted(accepted: Boolean) {
|
|
||||||
_appUiState.value = _appUiState.value.copy(
|
|
||||||
notificationPermissionAccepted = accepted,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.SystemBarStyle
|
import android.provider.Settings
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarData
|
import androidx.compose.material3.SnackbarData
|
||||||
@@ -22,43 +21,41 @@ import androidx.compose.material3.SnackbarResult
|
|||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.toArgb
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
|
import com.wireguard.android.backend.GoBackend
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.options.OptionsScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -67,98 +64,83 @@ class MainActivity : AppCompatActivity() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var dataStoreManager: DataStoreManager
|
lateinit var dataStoreManager: DataStoreManager
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
lateinit var settingsRepository: SettingsRepository
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var serviceManager: ServiceManager
|
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
)
|
)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
|
|
||||||
|
|
||||||
// load preferences into memory and init data
|
// load preferences into memory and init data
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
dataStoreManager.init()
|
try {
|
||||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity)
|
dataStoreManager.init()
|
||||||
val settings = settingsRepository.getSettings()
|
if (settingsRepository.getAll().isEmpty()) {
|
||||||
if (settings.isAutoTunnelEnabled) {
|
settingsRepository.save(com.zaneschepke.wireguardautotunnel.data.model.Settings())
|
||||||
serviceManager.startWatcherService(application.applicationContext)
|
}
|
||||||
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e("Failed to load preferences")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setContent {
|
setContent {
|
||||||
val appViewModel = hiltViewModel<AppViewModel>()
|
// val activityViewModel = hiltViewModel<ActivityViewModel>()
|
||||||
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
val notificationPermissionState =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
|
||||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) else null
|
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
|
|
||||||
val vpnActivityResultState =
|
|
||||||
rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult(),
|
|
||||||
onResult = {
|
|
||||||
val accepted = (it.resultCode == RESULT_OK)
|
|
||||||
if (accepted) {
|
|
||||||
appViewModel.onVpnPermissionAccepted()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
fun showSnackBarMessage(message: StringValue) {
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
val result =
|
|
||||||
snackbarHostState.showSnackbar(
|
|
||||||
message = message.asString(this@MainActivity),
|
|
||||||
duration = SnackbarDuration.Short,
|
|
||||||
)
|
|
||||||
when (result) {
|
|
||||||
SnackbarResult.ActionPerformed,
|
|
||||||
SnackbarResult.Dismissed -> {
|
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(appUiState.requestPermissions) {
|
|
||||||
if (appUiState.requestPermissions) {
|
|
||||||
appViewModel.permissionsRequested()
|
|
||||||
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
|
|
||||||
) {
|
|
||||||
showSnackBarMessage(StringValue.StringResource(R.string.notification_permission_required))
|
|
||||||
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
|
|
||||||
}
|
|
||||||
if (!appUiState.vpnPermissionAccepted) {
|
|
||||||
return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WireguardAutoTunnelTheme {
|
WireguardAutoTunnelTheme {
|
||||||
LaunchedEffect(Unit) {
|
TransparentSystemBars()
|
||||||
appViewModel.setNotificationPermissionAccepted(
|
|
||||||
notificationPermissionState?.status?.isGranted ?: true,
|
|
||||||
)
|
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
if (!appUiState.snackbarMessageConsumed) {
|
|
||||||
showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage))
|
val notificationPermissionState =
|
||||||
appViewModel.snackbarMessageConsumed()
|
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
|
||||||
|
fun requestNotificationPermission() {
|
||||||
|
if (
|
||||||
|
!notificationPermissionState.status.isGranted &&
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
||||||
|
) {
|
||||||
|
notificationPermissionState.launchPermissionRequest()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showSnackBarMessage(message: String) {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
val result =
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = message,
|
||||||
|
actionLabel = applicationContext.getString(R.string.okay),
|
||||||
|
duration = SnackbarDuration.Short,
|
||||||
|
)
|
||||||
|
when (result) {
|
||||||
|
SnackbarResult.ActionPerformed,
|
||||||
|
SnackbarResult.Dismissed -> {
|
||||||
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
@@ -167,47 +149,65 @@ class MainActivity : AppCompatActivity() {
|
|||||||
snackbarData.visuals.message,
|
snackbarData.visuals.message,
|
||||||
isRtl = false,
|
isRtl = false,
|
||||||
containerColor =
|
containerColor =
|
||||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||||
2.dp,
|
2.dp,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
//TODO refactor
|
modifier = Modifier.focusable().focusProperties { up = focusRequester },
|
||||||
modifier = Modifier
|
bottomBar =
|
||||||
.focusable()
|
if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
||||||
.focusProperties {
|
{
|
||||||
when (navBackStackEntry?.destination?.route) {
|
BottomNavBar(
|
||||||
Screen.Lock.route -> Unit
|
navController,
|
||||||
else -> up = focusRequester
|
listOf(
|
||||||
|
Screen.Main.navItem,
|
||||||
|
Screen.Settings.navItem,
|
||||||
|
Screen.Support.navItem,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
{}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
|
||||||
BottomNavBar(
|
|
||||||
navController,
|
|
||||||
listOf(
|
|
||||||
Screen.Main.navItem,
|
|
||||||
Screen.Settings.navItem,
|
|
||||||
Screen.Support.navItem,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { padding ->
|
) { padding ->
|
||||||
NavHost(
|
if (vpnIntent != null) {
|
||||||
navController,
|
PermissionRequestFailedScreen(
|
||||||
startDestination =
|
padding = padding,
|
||||||
(if (PinManager.pinExists()) Screen.Lock.route else Screen.Main.route),
|
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
|
||||||
modifier =
|
message = getString(R.string.vpn_permission_required),
|
||||||
Modifier
|
getString(R.string.retry),
|
||||||
.padding(padding)
|
)
|
||||||
.fillMaxSize(),
|
return@Scaffold
|
||||||
) {
|
}
|
||||||
|
if (!notificationPermissionState.status.isGranted) {
|
||||||
|
PermissionRequestFailedScreen(
|
||||||
|
padding = padding,
|
||||||
|
onRequestAgain = {
|
||||||
|
val intentSettings =
|
||||||
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
|
intentSettings.data =
|
||||||
|
Uri.fromParts(
|
||||||
|
Constants.URI_PACKAGE_SCHEME,
|
||||||
|
this.packageName,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
startActivity(intentSettings)
|
||||||
|
},
|
||||||
|
message = getString(R.string.notification_permission_required),
|
||||||
|
getString(R.string.open_settings),
|
||||||
|
)
|
||||||
|
return@Scaffold
|
||||||
|
}
|
||||||
|
NavHost(navController, startDestination = Screen.Main.route) {
|
||||||
composable(
|
composable(
|
||||||
Screen.Main.route,
|
Screen.Main.route,
|
||||||
) {
|
) {
|
||||||
MainScreen(
|
MainScreen(
|
||||||
|
padding = padding,
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
appViewModel = appViewModel,
|
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||||
navController = navController,
|
navController = navController,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -215,8 +215,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
Screen.Settings.route,
|
Screen.Settings.route,
|
||||||
) {
|
) {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
appViewModel = appViewModel,
|
padding = padding,
|
||||||
navController = navController,
|
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -224,42 +224,25 @@ class MainActivity : AppCompatActivity() {
|
|||||||
Screen.Support.route,
|
Screen.Support.route,
|
||||||
) {
|
) {
|
||||||
SupportScreen(
|
SupportScreen(
|
||||||
|
padding = padding,
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
appViewModel = appViewModel,
|
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||||
navController = navController,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Screen.Support.Logs.route) {
|
|
||||||
LogsScreen(appViewModel)
|
|
||||||
}
|
|
||||||
composable("${Screen.Config.route}/{id}") {
|
composable("${Screen.Config.route}/{id}") {
|
||||||
val id = it.arguments?.getString("id")
|
val id = it.arguments?.getString("id")
|
||||||
if (!id.isNullOrBlank()) {
|
if (!id.isNullOrBlank()) {
|
||||||
|
// https://dagger.dev/hilt/view-model#assisted-injection
|
||||||
ConfigScreen(
|
ConfigScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
tunnelId = id,
|
id = id,
|
||||||
appViewModel = appViewModel,
|
showSnackbarMessage = { message ->
|
||||||
|
showSnackBarMessage(message)
|
||||||
|
},
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable("${Screen.Option.route}/{id}") {
|
|
||||||
val id = it.arguments?.getString("id")
|
|
||||||
if (!id.isNullOrBlank()) {
|
|
||||||
OptionsScreen(
|
|
||||||
navController = navController,
|
|
||||||
tunnelId = id,
|
|
||||||
appViewModel = appViewModel,
|
|
||||||
focusRequester = focusRequester,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
composable(Screen.Lock.route) {
|
|
||||||
PinLockScreen(
|
|
||||||
navController = navController,
|
|
||||||
appViewModel = appViewModel,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,12 +32,7 @@ sealed class Screen(val route: String) {
|
|||||||
route = route,
|
route = route,
|
||||||
icon = Icons.Rounded.QuestionMark,
|
icon = Icons.Rounded.QuestionMark,
|
||||||
)
|
)
|
||||||
|
|
||||||
data object Logs : Screen("support/logs")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data object Config : Screen("config")
|
data object Config : Screen("config")
|
||||||
data object Lock : Screen("lock")
|
|
||||||
|
|
||||||
data object Option : Screen("option")
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-5
@@ -10,6 +10,8 @@ import androidx.compose.material3.TextButton
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClickableIconButton(
|
fun ClickableIconButton(
|
||||||
@@ -27,12 +29,9 @@ fun ClickableIconButton(
|
|||||||
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = icon.name,
|
contentDescription = stringResource(R.string.delete),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
||||||
.size(ButtonDefaults.IconSize)
|
|
||||||
.weight(1f, false)
|
|
||||||
.clickable {
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
onIconClick()
|
onIconClick()
|
||||||
}
|
}
|
||||||
|
|||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,24 +34,22 @@ fun RowListItem(
|
|||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.animateContentSize()
|
||||||
.animateContentSize()
|
.clip(RoundedCornerShape(30.dp))
|
||||||
.clip(RoundedCornerShape(30.dp))
|
.combinedClickable(
|
||||||
.combinedClickable(
|
onClick = { onClick() },
|
||||||
onClick = { onClick() },
|
onLongClick = { onHold() },
|
||||||
onLongClick = { onHold() },
|
),
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp, vertical = 5.dp),
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 15.dp, vertical = 5.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth(.60f),
|
||||||
) {
|
) {
|
||||||
icon()
|
icon()
|
||||||
Text(text)
|
Text(text)
|
||||||
@@ -62,13 +60,11 @@ fun RowListItem(
|
|||||||
statistics?.peers()?.forEach {
|
statistics?.peers()?.forEach {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
||||||
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
) {
|
) {
|
||||||
//TODO change these to string resources
|
|
||||||
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||||
val peerTx = statistics.peer(it)!!.txBytes
|
val peerTx = statistics.peer(it)!!.txBytes
|
||||||
val peerRx = statistics.peer(it)!!.rxBytes
|
val peerRx = statistics.peer(it)!!.rxBytes
|
||||||
|
|||||||
@@ -63,18 +63,17 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
|||||||
},
|
},
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
colors =
|
colors =
|
||||||
TextFieldDefaults.colors(
|
TextFieldDefaults.colors(
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
unfocusedContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
disabledContainerColor = Color.Transparent,
|
disabledContainerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
|
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
|
||||||
textStyle = MaterialTheme.typography.bodySmall,
|
textStyle = MaterialTheme.typography.bodySmall,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
|
||||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -27,10 +27,10 @@ fun ConfigurationTextBox(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
placeholder = { Text(hint) },
|
placeholder = { Text(hint) },
|
||||||
keyboardOptions =
|
keyboardOptions =
|
||||||
KeyboardOptions(
|
KeyboardOptions(
|
||||||
capitalization = KeyboardCapitalization.None,
|
capitalization = KeyboardCapitalization.None,
|
||||||
imeAction = ImeAction.Done,
|
imeAction = ImeAction.Done,
|
||||||
),
|
),
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-5
@@ -9,7 +9,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -22,13 +21,11 @@ fun ConfigurationToggle(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().padding(padding),
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(padding),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Text(label, textAlign = TextAlign.Start)
|
Text(label)
|
||||||
Switch(
|
Switch(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
|||||||
+2
-17
@@ -6,33 +6,18 @@ import androidx.compose.material3.NavigationBar
|
|||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.graphics.Color
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
|
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
|
||||||
val backStackEntry = navController.currentBackStackEntryAsState()
|
val backStackEntry = navController.currentBackStackEntryAsState()
|
||||||
|
|
||||||
var showBottomBar by rememberSaveable { mutableStateOf(true) }
|
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
|
||||||
|
|
||||||
//TODO find a better way to hide nav bar
|
|
||||||
showBottomBar = when (navBackStackEntry?.destination?.route) {
|
|
||||||
Screen.Lock.route -> false
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
containerColor = if (!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
) {
|
) {
|
||||||
if (showBottomBar) bottomNavItems.forEach { item ->
|
bottomNavItems.forEach { item ->
|
||||||
val selected = item.route == backStackEntry.value?.destination?.route
|
val selected = item.route == backStackEntry.value?.destination?.route
|
||||||
|
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
|
|||||||
-6
@@ -21,32 +21,26 @@ fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (
|
|||||||
onError("Biometrics not available")
|
onError("Biometrics not available")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||||
onError("Biometrics not created")
|
onError("Biometrics not created")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
||||||
onError("Biometric hardware not found")
|
onError("Biometric hardware not found")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
||||||
onError("Biometric security update required")
|
onError("Biometric security update required")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
||||||
onError("Biometrics not supported")
|
onError("Biometrics not supported")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
||||||
onError("Biometrics status unknown")
|
onError("Biometrics status unknown")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -19,9 +19,12 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -30,30 +33,27 @@ fun CustomSnackBar(
|
|||||||
isRtl: Boolean = true,
|
isRtl: Boolean = true,
|
||||||
containerColor: Color = MaterialTheme.colorScheme.surface
|
containerColor: Color = MaterialTheme.colorScheme.surface
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
Snackbar(
|
Snackbar(
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth(
|
||||||
.fillMaxWidth(
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
|
)
|
||||||
)
|
.padding(bottom = 100.dp),
|
||||||
.padding(bottom = 100.dp),
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
|
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
|
||||||
.width(IntrinsicSize.Max)
|
|
||||||
.height(IntrinsicSize.Min),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
) {
|
) {
|
||||||
val icon = Icons.Rounded.Info
|
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
Icons.Rounded.Info,
|
||||||
contentDescription = icon.name,
|
contentDescription = stringResource(R.string.info),
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.padding(end = 10.dp),
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-4
@@ -16,10 +16,7 @@ fun LoadingScreen() {
|
|||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().focusable().padding(),
|
||||||
.fillMaxSize()
|
|
||||||
.focusable()
|
|
||||||
.padding(),
|
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
Column(modifier = Modifier.padding(120.dp)) { CircularProgressIndicator() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common.text
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LogTypeLabel(color: Color, content: @Composable () -> Unit) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(20.dp)
|
|
||||||
.clip(RoundedCornerShape(2.dp))
|
|
||||||
.background(color),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -15,7 +15,7 @@ import androidx.compose.ui.unit.sp
|
|||||||
fun SectionTitle(title: String, padding: Dp) {
|
fun SectionTitle(title: String, padding: Dp) {
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Center,
|
||||||
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
|
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
|
||||||
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
|
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ data class InterfaceProxy(
|
|||||||
addresses = i.addresses.joinToString(", ").trim(),
|
addresses = i.addresses.joinToString(", ").trim(),
|
||||||
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
||||||
listenPort =
|
listenPort =
|
||||||
if (i.listenPort.isPresent) {
|
if (i.listenPort.isPresent) {
|
||||||
i.listenPort.get().toString().trim()
|
i.listenPort.get().toString().trim()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
|
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,23 +14,23 @@ data class PeerProxy(
|
|||||||
return PeerProxy(
|
return PeerProxy(
|
||||||
publicKey = peer.publicKey.toBase64(),
|
publicKey = peer.publicKey.toBase64(),
|
||||||
preSharedKey =
|
preSharedKey =
|
||||||
if (peer.preSharedKey.isPresent) {
|
if (peer.preSharedKey.isPresent) {
|
||||||
peer.preSharedKey.get().toBase64().trim()
|
peer.preSharedKey.get().toBase64().trim()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
persistentKeepalive =
|
persistentKeepalive =
|
||||||
if (peer.persistentKeepalive.isPresent) {
|
if (peer.persistentKeepalive.isPresent) {
|
||||||
peer.persistentKeepalive.get().toString().trim()
|
peer.persistentKeepalive.get().toString().trim()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
endpoint =
|
endpoint =
|
||||||
if (peer.endpoint.isPresent) {
|
if (peer.endpoint.isPresent) {
|
||||||
peer.endpoint.get().toString().trim()
|
peer.endpoint.get().toString().trim()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
allowedIps = peer.allowedIps.joinToString(", ").trim(),
|
allowedIps = peer.allowedIps.joinToString(", ").trim(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-114
@@ -1,6 +1,7 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
@@ -28,7 +29,7 @@ import androidx.compose.material.icons.rounded.ContentCopy
|
|||||||
import androidx.compose.material.icons.rounded.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
import androidx.compose.material.icons.rounded.Refresh
|
||||||
import androidx.compose.material.icons.rounded.Save
|
import androidx.compose.material.icons.rounded.Save
|
||||||
import androidx.compose.material3.BasicAlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
@@ -50,6 +51,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
@@ -61,7 +63,6 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
@@ -72,7 +73,6 @@ import androidx.navigation.NavController
|
|||||||
import com.google.accompanist.drawablepainter.DrawablePainter
|
import com.google.accompanist.drawablepainter.DrawablePainter
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||||
@@ -86,15 +86,17 @@ import kotlinx.coroutines.delay
|
|||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@OptIn(
|
@OptIn(
|
||||||
|
ExperimentalComposeUiApi::class,
|
||||||
ExperimentalMaterial3Api::class,
|
ExperimentalMaterial3Api::class,
|
||||||
|
ExperimentalFoundationApi::class,
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigScreen(
|
fun ConfigScreen(
|
||||||
viewModel: ConfigViewModel = hiltViewModel(),
|
viewModel: ConfigViewModel = hiltViewModel(),
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
appViewModel: AppViewModel,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
tunnelId: String
|
id: String
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||||
@@ -105,7 +107,7 @@ fun ConfigScreen(
|
|||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(Unit) { viewModel.init(tunnelId) }
|
LaunchedEffect(Unit) { viewModel.init(id) }
|
||||||
|
|
||||||
LaunchedEffect(uiState.loading) {
|
LaunchedEffect(uiState.loading) {
|
||||||
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
@@ -127,17 +129,14 @@ fun ConfigScreen(
|
|||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
val screenPadding = 5.dp
|
val screenPadding = 5.dp
|
||||||
|
|
||||||
val applicationButtonText = buildAnnotatedString {
|
val applicationButtonText = {
|
||||||
append(stringResource(id = R.string.tunneling_apps))
|
"Tunneling apps: " +
|
||||||
append(": ")
|
if (uiState.isAllApplicationsEnabled) {
|
||||||
if (uiState.isAllApplicationsEnabled) {
|
"all"
|
||||||
append(stringResource(id = R.string.all))
|
} else {
|
||||||
} else {
|
"${uiState.checkedPackageNames.size} " +
|
||||||
append("${uiState.checkedPackageNames.size} ")
|
(if (uiState.include) "included" else "excluded")
|
||||||
(if (uiState.include) append(stringResource(id = R.string.included)) else append(
|
}
|
||||||
stringResource(id = R.string.excluded),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showAuthPrompt) {
|
if (showAuthPrompt) {
|
||||||
@@ -146,13 +145,13 @@ fun ConfigScreen(
|
|||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
isAuthenticated = true
|
isAuthenticated = true
|
||||||
},
|
},
|
||||||
onError = {
|
onError = { error ->
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||||
},
|
},
|
||||||
onFailure = {
|
onFailure = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -162,26 +161,20 @@ fun ConfigScreen(
|
|||||||
remember(uiState.packages) {
|
remember(uiState.packages) {
|
||||||
uiState.packages.sortedBy { viewModel.getPackageLabel(it) }
|
uiState.packages.sortedBy { viewModel.getPackageLabel(it) }
|
||||||
}
|
}
|
||||||
BasicAlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
AlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
||||||
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
@@ -194,9 +187,8 @@ fun ConfigScreen(
|
|||||||
if (!uiState.isAllApplicationsEnabled) {
|
if (!uiState.isAllApplicationsEnabled) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
@@ -227,9 +219,8 @@ fun ConfigScreen(
|
|||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
@@ -245,9 +236,7 @@ fun ConfigScreen(
|
|||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().padding(5.dp),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(5.dp),
|
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
|
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
|
||||||
val drawable =
|
val drawable =
|
||||||
@@ -259,10 +248,9 @@ fun ConfigScreen(
|
|||||||
modifier = Modifier.size(50.dp, 50.dp),
|
modifier = Modifier.size(50.dp, 50.dp),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val icon = Icons.Rounded.Android
|
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
Icons.Rounded.Android,
|
||||||
icon.name,
|
stringResource(id = R.string.edit),
|
||||||
modifier = Modifier.size(50.dp, 50.dp),
|
modifier = Modifier.size(50.dp, 50.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -274,9 +262,9 @@ fun ConfigScreen(
|
|||||||
Checkbox(
|
Checkbox(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
checked =
|
checked =
|
||||||
(uiState.checkedPackageNames.contains(
|
(uiState.checkedPackageNames.contains(
|
||||||
pack.packageName,
|
pack.packageName
|
||||||
)),
|
)),
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
if (it) {
|
if (it) {
|
||||||
viewModel.onAddCheckedPackage(pack.packageName)
|
viewModel.onAddCheckedPackage(pack.packageName)
|
||||||
@@ -291,9 +279,7 @@ fun ConfigScreen(
|
|||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(top = 5.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = { showApplicationsDialog = false }) {
|
TextButton(onClick = { showApplicationsDialog = false }) {
|
||||||
@@ -313,20 +299,19 @@ fun ConfigScreen(
|
|||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.onFocusChanged {
|
Modifier.padding(bottom = 90.dp).onFocusChanged {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onSaveAllChanges().let {
|
viewModel.onSaveAllChanges().let {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Result.Success -> {
|
is Result.Success -> {
|
||||||
appViewModel.showSnackbarMessage(it.data.message)
|
showSnackbarMessage(it.data.message)
|
||||||
navController.navigate(Screen.Main.route)
|
navController.navigate(Screen.Main.route)
|
||||||
}
|
}
|
||||||
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -346,10 +331,7 @@ fun ConfigScreen(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize(),
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.weight(1f, true)
|
|
||||||
.fillMaxSize(),
|
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
@@ -357,21 +339,17 @@ fun ConfigScreen(
|
|||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier
|
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
||||||
.fillMaxHeight(fillMaxHeight)
|
} else {
|
||||||
.fillMaxWidth(fillMaxWidth)
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
} else {
|
})
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
.padding(top = 50.dp, bottom = 10.dp),
|
||||||
})
|
|
||||||
.padding(bottom = 10.dp),
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(15.dp).focusGroup(),
|
||||||
.padding(15.dp)
|
|
||||||
.focusGroup(),
|
|
||||||
) {
|
) {
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
stringResource(R.string.interface_),
|
stringResource(R.string.interface_),
|
||||||
@@ -383,20 +361,16 @@ fun ConfigScreen(
|
|||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.name),
|
label = stringResource(R.string.name),
|
||||||
hint = stringResource(R.string.tunnel_name).lowercase(),
|
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().clickable { showAuthPrompt = true },
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { showAuthPrompt = true },
|
|
||||||
value = uiState.interfaceProxy.privateKey,
|
value = uiState.interfaceProxy.privateKey,
|
||||||
visualTransformation =
|
visualTransformation =
|
||||||
if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
||||||
VisualTransformation.None
|
VisualTransformation.None
|
||||||
else PasswordVisualTransformation(),
|
else PasswordVisualTransformation(),
|
||||||
enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||||
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -418,9 +392,7 @@ fun ConfigScreen(
|
|||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth().focusRequester(FocusRequester.Default),
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(FocusRequester.Default),
|
|
||||||
value = uiState.interfaceProxy.publicKey,
|
value = uiState.interfaceProxy.publicKey,
|
||||||
enabled = false,
|
enabled = false,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
@@ -453,9 +425,7 @@ fun ConfigScreen(
|
|||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.addresses),
|
label = stringResource(R.string.addresses),
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
|
||||||
.fillMaxWidth(3 / 5f)
|
|
||||||
.padding(end = 5.dp),
|
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.listenPort,
|
value = uiState.interfaceProxy.listenPort,
|
||||||
@@ -473,9 +443,7 @@ fun ConfigScreen(
|
|||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.dns_servers),
|
label = stringResource(R.string.dns_servers),
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
|
||||||
.fillMaxWidth(3 / 5f)
|
|
||||||
.padding(end = 5.dp),
|
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.mtu,
|
value = uiState.interfaceProxy.mtu,
|
||||||
@@ -488,13 +456,11 @@ fun ConfigScreen(
|
|||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(top = 5.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = { showApplicationsDialog = true }) {
|
TextButton(onClick = { showApplicationsDialog = true }) {
|
||||||
Text(applicationButtonText.text)
|
Text(applicationButtonText())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -506,36 +472,29 @@ fun ConfigScreen(
|
|||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier
|
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
||||||
.fillMaxHeight(fillMaxHeight)
|
} else {
|
||||||
.fillMaxWidth(fillMaxWidth)
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
} else {
|
})
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
.padding(top = 10.dp, bottom = 10.dp),
|
||||||
})
|
|
||||||
.padding(top = 10.dp, bottom = 10.dp),
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp),
|
||||||
.padding(horizontal = 15.dp)
|
|
||||||
.padding(bottom = 10.dp),
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp),
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 5.dp),
|
|
||||||
) {
|
) {
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
stringResource(R.string.peer),
|
stringResource(R.string.peer),
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
)
|
)
|
||||||
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
|
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
|
||||||
val icon = Icons.Rounded.Delete
|
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,9 +570,7 @@ fun ConfigScreen(
|
|||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(bottom = 140.dp),
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
+52
-47
@@ -14,7 +14,8 @@ import com.wireguard.crypto.Key
|
|||||||
import com.wireguard.crypto.KeyPair
|
import com.wireguard.crypto.KeyPair
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
@@ -27,8 +28,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -36,7 +37,8 @@ class ConfigViewModel
|
|||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val appDataRepository: AppDataRepository
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
|
private val settingsRepository: SettingsRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val packageManager = application.packageManager
|
private val packageManager = application.packageManager
|
||||||
@@ -50,8 +52,7 @@ constructor(
|
|||||||
val state =
|
val state =
|
||||||
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||||
val tunnelConfig =
|
val tunnelConfig =
|
||||||
appDataRepository.tunnels.getAll()
|
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
||||||
.firstOrNull { it.id.toString() == tunnelId }
|
|
||||||
if (tunnelConfig != null) {
|
if (tunnelConfig != null) {
|
||||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||||
@@ -99,7 +100,7 @@ constructor(
|
|||||||
fun onAddCheckedPackage(packageName: String) {
|
fun onAddCheckedPackage(packageName: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
checkedPackageNames = _uiState.value.checkedPackageNames + packageName,
|
checkedPackageNames = _uiState.value.checkedPackageNames + packageName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ constructor(
|
|||||||
fun onRemoveCheckedPackage(packageName: String) {
|
fun onRemoveCheckedPackage(packageName: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
checkedPackageNames = _uiState.value.checkedPackageNames - packageName,
|
checkedPackageNames = _uiState.value.checkedPackageNames - packageName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,16 +145,26 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
||||||
viewModelScope.launch { appDataRepository.tunnels.save(tunnelConfig) }
|
viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) }
|
||||||
|
|
||||||
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (tunnelConfig != null) {
|
if (tunnelConfig != null) {
|
||||||
saveConfig(tunnelConfig).join()
|
saveConfig(tunnelConfig).join()
|
||||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
|
updateSettingsDefaultTunnel(tunnelConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun updateSettingsDefaultTunnel(tunnelConfig: TunnelConfig) {
|
||||||
|
val settings = settingsRepository.getSettingsFlow().first()
|
||||||
|
if (settings.defaultTunnel != null) {
|
||||||
|
if (tunnelConfig.id == TunnelConfig.from(settings.defaultTunnel!!).id) {
|
||||||
|
settingsRepository.save(settings.copy(defaultTunnel = tunnelConfig.toString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
private fun buildPeerListFromProxyPeers(): List<Peer> {
|
||||||
return _uiState.value.proxyPeers.map {
|
return _uiState.value.proxyPeers.map {
|
||||||
val builder = Peer.Builder()
|
val builder = Peer.Builder()
|
||||||
@@ -195,12 +206,8 @@ constructor(
|
|||||||
val peerList = buildPeerListFromProxyPeers()
|
val peerList = buildPeerListFromProxyPeers()
|
||||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||||
val tunnelConfig = when (uiState.value.tunnel) {
|
val tunnelConfig = when(uiState.value.tunnel) {
|
||||||
null -> TunnelConfig(
|
null -> TunnelConfig(name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
|
||||||
name = _uiState.value.tunnelName,
|
|
||||||
wgQuick = config.toWgQuickString(),
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> uiState.value.tunnel!!.copy(
|
else -> uiState.value.tunnel!!.copy(
|
||||||
name = _uiState.value.tunnelName,
|
name = _uiState.value.tunnelName,
|
||||||
wgQuick = config.toWgQuickString(),
|
wgQuick = config.toWgQuickString(),
|
||||||
@@ -209,9 +216,7 @@ constructor(
|
|||||||
updateTunnelConfig(tunnelConfig)
|
updateTunnelConfig(tunnelConfig)
|
||||||
Result.Success(Event.Message.ConfigSaved)
|
Result.Success(Event.Message.ConfigSaved)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Result.Error(Event.Error.Exception(e))
|
||||||
val message = e.message?.substringAfter(":", missingDelimiterValue = "")
|
|
||||||
Result.Error(Event.Error.ConfigParseError(message ?: ""))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,10 +224,10 @@ constructor(
|
|||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index,
|
index,
|
||||||
_uiState.value.proxyPeers[index].copy(publicKey = value),
|
_uiState.value.proxyPeers[index].copy(publicKey = value),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,10 +235,10 @@ constructor(
|
|||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index,
|
index,
|
||||||
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
|
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,10 +246,10 @@ constructor(
|
|||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index,
|
index,
|
||||||
_uiState.value.proxyPeers[index].copy(endpoint = value),
|
_uiState.value.proxyPeers[index].copy(endpoint = value),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,10 +257,10 @@ constructor(
|
|||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index,
|
index,
|
||||||
_uiState.value.proxyPeers[index].copy(allowedIps = value),
|
_uiState.value.proxyPeers[index].copy(allowedIps = value),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,10 +268,10 @@ constructor(
|
|||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
proxyPeers =
|
proxyPeers =
|
||||||
_uiState.value.proxyPeers.update(
|
_uiState.value.proxyPeers.update(
|
||||||
index,
|
index,
|
||||||
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
|
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,31 +291,31 @@ constructor(
|
|||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy =
|
interfaceProxy =
|
||||||
_uiState.value.interfaceProxy.copy(
|
_uiState.value.interfaceProxy.copy(
|
||||||
privateKey = keyPair.privateKey.toBase64(),
|
privateKey = keyPair.privateKey.toBase64(),
|
||||||
publicKey = keyPair.publicKey.toBase64(),
|
publicKey = keyPair.publicKey.toBase64(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAddressesChanged(value: String) {
|
fun onAddressesChanged(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value),
|
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onListenPortChanged(value: String) {
|
fun onListenPortChanged(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value),
|
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDnsServersChanged(value: String) {
|
fun onDnsServersChanged(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value),
|
interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,14 +327,14 @@ constructor(
|
|||||||
private fun onInterfacePublicKeyChange(value: String) {
|
private fun onInterfacePublicKeyChange(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value),
|
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPrivateKeyChange(value: String) {
|
fun onPrivateKeyChange(value: String) {
|
||||||
_uiState.value =
|
_uiState.value =
|
||||||
_uiState.value.copy(
|
_uiState.value.copy(
|
||||||
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value),
|
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value)
|
||||||
)
|
)
|
||||||
if (NumberUtils.isValidKey(value)) {
|
if (NumberUtils.isValidKey(value)) {
|
||||||
val pair = KeyPair(Key.fromBase64(value))
|
val pair = KeyPair(Key.fromBase64(value))
|
||||||
|
|||||||
+245
-210
@@ -19,10 +19,13 @@ import androidx.compose.foundation.gestures.ScrollableDefaults
|
|||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredWidth
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -36,25 +39,25 @@ import androidx.compose.material.icons.filled.QrCode
|
|||||||
import androidx.compose.material.icons.rounded.Add
|
import androidx.compose.material.icons.rounded.Add
|
||||||
import androidx.compose.material.icons.rounded.Bolt
|
import androidx.compose.material.icons.rounded.Bolt
|
||||||
import androidx.compose.material.icons.rounded.Circle
|
import androidx.compose.material.icons.rounded.Circle
|
||||||
import androidx.compose.material.icons.rounded.CopyAll
|
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
|
import androidx.compose.material.icons.rounded.Edit
|
||||||
import androidx.compose.material.icons.rounded.Info
|
import androidx.compose.material.icons.rounded.Info
|
||||||
import androidx.compose.material.icons.rounded.Settings
|
|
||||||
import androidx.compose.material.icons.rounded.Smartphone
|
|
||||||
import androidx.compose.material.icons.rounded.Star
|
import androidx.compose.material.icons.rounded.Star
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.typography
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -74,10 +77,10 @@ import androidx.compose.ui.focus.onFocusChanged
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
@@ -90,7 +93,6 @@ import com.zaneschepke.wireguardautotunnel.R
|
|||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||||
@@ -102,18 +104,19 @@ import com.zaneschepke.wireguardautotunnel.util.Event
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||||
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
viewModel: MainViewModel = hiltViewModel(),
|
viewModel: MainViewModel = hiltViewModel(),
|
||||||
appViewModel: AppViewModel,
|
padding: PaddingValues,
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
|
showSnackbarMessage: (String) -> Unit,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
@@ -124,17 +127,23 @@ fun MainScreen(
|
|||||||
val sheetState = rememberModalBottomSheetState()
|
val sheetState = rememberModalBottomSheetState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
||||||
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(uiState.loading) {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (uiState.loading) {
|
||||||
|
LoadingScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val tunnelFileImportResultLauncher =
|
val tunnelFileImportResultLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(
|
||||||
object : ActivityResultContracts.GetContent() {
|
object : ActivityResultContracts.GetContent() {
|
||||||
@@ -164,7 +173,7 @@ fun MainScreen(
|
|||||||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||||
}
|
}
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
@@ -174,7 +183,7 @@ fun MainScreen(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.onTunnelFileSelected(data).let {
|
viewModel.onTunnelFileSelected(data).let {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
is Result.Success -> {}
|
is Result.Success -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,7 +198,7 @@ fun MainScreen(
|
|||||||
viewModel.onTunnelQrResult(it.contents).let { result ->
|
viewModel.onTunnelQrResult(it.contents).let { result ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is Result.Success -> {}
|
is Result.Success -> {}
|
||||||
is Result.Error -> appViewModel.showSnackbarMessage(result.error.message)
|
is Result.Error -> showSnackbarMessage(result.error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,6 +206,30 @@ fun MainScreen(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(showPrimaryChangeAlertDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showPrimaryChangeAlertDialog = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.onDefaultTunnelChange(selectedTunnel)
|
||||||
|
showPrimaryChangeAlertDialog = false
|
||||||
|
selectedTunnel = null
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.okay))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showPrimaryChangeAlertDialog = false }) {
|
||||||
|
Text(text = stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text(text = stringResource(R.string.primary_tunnel_change)) },
|
||||||
|
text = { Text(text = stringResource(R.string.primary_tunnel_change_question)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(showDeleteTunnelAlertDialog) {
|
AnimatedVisibility(showDeleteTunnelAlertDialog) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showDeleteTunnelAlertDialog = false },
|
onDismissRequest = { showDeleteTunnelAlertDialog = false },
|
||||||
@@ -222,25 +255,63 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||||
if (appViewModel.isRequiredPermissionGranted()) {
|
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
||||||
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiState.loading) {
|
|
||||||
return LoadingScreen()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.pointerInput(Unit) {
|
Modifier.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onTap = {
|
onTap = {
|
||||||
selectedTunnel = null
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
|
topBar = {
|
||||||
|
if (uiState.settings.isAutoTunnelEnabled)
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp)
|
||||||
|
.padding(end = 5.dp),
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Bolt,
|
||||||
|
stringResource(id = R.string.auto),
|
||||||
|
modifier = Modifier.size(25.dp),
|
||||||
|
tint =
|
||||||
|
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
||||||
|
else mint,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Auto-tunneling: ${if (uiState.settings.isAutoTunnelPaused) "paused" else "active"}",
|
||||||
|
style = typography.bodyLarge,
|
||||||
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (uiState.settings.isAutoTunnelPaused)
|
||||||
|
TextButton(
|
||||||
|
onClick = { viewModel.resumeAutoTunneling() },
|
||||||
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
|
) {
|
||||||
|
Text("Resume")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
TextButton(
|
||||||
|
onClick = { viewModel.pauseAutoTunneling() },
|
||||||
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
|
) {
|
||||||
|
Text("Pause")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isVisible.value,
|
visible = isVisible.value,
|
||||||
@@ -252,17 +323,18 @@ fun MainScreen(
|
|||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
modifier =
|
modifier =
|
||||||
(if (
|
(if (
|
||||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||||
uiState.tunnels.isEmpty()
|
uiState.tunnels.isEmpty()
|
||||||
)
|
)
|
||||||
Modifier.focusRequester(focusRequester)
|
Modifier.focusRequester(focusRequester)
|
||||||
else Modifier)
|
else Modifier)
|
||||||
.onFocusChanged {
|
.padding(bottom = 90.dp)
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
.onFocusChanged {
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
}
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
},
|
}
|
||||||
|
},
|
||||||
onClick = { showBottomSheet = true },
|
onClick = { showBottomSheet = true },
|
||||||
containerColor = fobColor,
|
containerColor = fobColor,
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
@@ -275,13 +347,12 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) { innerPadding ->
|
||||||
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
.fillMaxSize(),
|
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
||||||
}
|
}
|
||||||
@@ -294,13 +365,12 @@ fun MainScreen(
|
|||||||
// Sheet content
|
// Sheet content
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.clickable {
|
||||||
.clickable {
|
showBottomSheet = false
|
||||||
showBottomSheet = false
|
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
}
|
||||||
}
|
.padding(10.dp),
|
||||||
.padding(10.dp),
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.FileOpen,
|
Icons.Filled.FileOpen,
|
||||||
@@ -313,27 +383,26 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
HorizontalDivider()
|
Divider()
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.clickable {
|
||||||
.clickable {
|
scope.launch {
|
||||||
scope.launch {
|
showBottomSheet = false
|
||||||
showBottomSheet = false
|
val scanOptions = ScanOptions()
|
||||||
val scanOptions = ScanOptions()
|
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
scanOptions.setOrientationLocked(true)
|
||||||
scanOptions.setOrientationLocked(true)
|
scanOptions.setPrompt(
|
||||||
scanOptions.setPrompt(
|
context.getString(R.string.scanning_qr)
|
||||||
context.getString(R.string.scanning_qr),
|
)
|
||||||
)
|
scanOptions.setBeepEnabled(false)
|
||||||
scanOptions.setBeepEnabled(false)
|
scanOptions.captureActivity =
|
||||||
scanOptions.captureActivity =
|
CaptureActivityPortrait::class.java
|
||||||
CaptureActivityPortrait::class.java
|
scanLauncher.launch(scanOptions)
|
||||||
scanLauncher.launch(scanOptions)
|
}
|
||||||
}
|
}
|
||||||
}
|
.padding(10.dp),
|
||||||
.padding(10.dp),
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.QrCode,
|
Icons.Filled.QrCode,
|
||||||
@@ -346,18 +415,17 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider()
|
Divider()
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.clickable {
|
||||||
.clickable {
|
showBottomSheet = false
|
||||||
showBottomSheet = false
|
navController.navigate(
|
||||||
navController.navigate(
|
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
||||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
)
|
||||||
)
|
}
|
||||||
}
|
.padding(10.dp),
|
||||||
.padding(10.dp),
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Create,
|
Icons.Filled.Create,
|
||||||
@@ -376,70 +444,23 @@ fun MainScreen(
|
|||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.fillMaxHeight(.90f)
|
||||||
.overscroll(ScrollableDefaults.overscrollEffect()),
|
.overscroll(ScrollableDefaults.overscrollEffect())
|
||||||
|
.padding(innerPadding),
|
||||||
state = rememberLazyListState(0, uiState.tunnels.count()),
|
state = rememberLazyListState(0, uiState.tunnels.count()),
|
||||||
userScrollEnabled = true,
|
userScrollEnabled = true,
|
||||||
reverseLayout = false,
|
reverseLayout = true,
|
||||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||||
) {
|
) {
|
||||||
item {
|
|
||||||
if (uiState.settings.isAutoTunnelEnabled) {
|
|
||||||
val autoTunnelingLabel = buildAnnotatedString {
|
|
||||||
append(stringResource(id = R.string.auto_tunneling))
|
|
||||||
append(": ")
|
|
||||||
if (uiState.settings.isAutoTunnelPaused) append(
|
|
||||||
stringResource(id = R.string.paused),
|
|
||||||
) else append(
|
|
||||||
stringResource(id = R.string.active),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
RowListItem(
|
|
||||||
icon = {
|
|
||||||
val icon = Icons.Rounded.Bolt
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
icon.name,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(end = 8.5.dp)
|
|
||||||
.size(25.dp),
|
|
||||||
tint =
|
|
||||||
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
|
||||||
else mint,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = autoTunnelingLabel.text,
|
|
||||||
rowButton = {
|
|
||||||
if (uiState.settings.isAutoTunnelPaused) {
|
|
||||||
TextButton(
|
|
||||||
onClick = { viewModel.resumeAutoTunneling() },
|
|
||||||
) {
|
|
||||||
Text(stringResource(id = R.string.resume))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TextButton(
|
|
||||||
onClick = { viewModel.pauseAutoTunneling() },
|
|
||||||
) {
|
|
||||||
Text(stringResource(id = R.string.pause))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {},
|
|
||||||
onHold = {},
|
|
||||||
expanded = false,
|
|
||||||
statistics = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items(
|
items(
|
||||||
uiState.tunnels,
|
uiState.tunnels,
|
||||||
key = { tunnel -> tunnel.id },
|
key = { tunnel -> tunnel.id },
|
||||||
) { tunnel ->
|
) { tunnel ->
|
||||||
val leadingIconColor =
|
val leadingIconColor =
|
||||||
(if (
|
(if (
|
||||||
uiState.vpnState.tunnelConfig?.name == tunnel.name &&
|
uiState.vpnState.name == tunnel.name &&
|
||||||
uiState.vpnState.status == Tunnel.State.UP
|
uiState.vpnState.status == Tunnel.State.UP
|
||||||
) {
|
) {
|
||||||
uiState.vpnState.statistics
|
uiState.vpnState.statistics
|
||||||
?.mapPeerStats()
|
?.mapPeerStats()
|
||||||
@@ -450,7 +471,6 @@ fun MainScreen(
|
|||||||
statuses?.any { it == HandshakeStatus.STALE } == true -> corn
|
statuses?.any { it == HandshakeStatus.STALE } == true -> corn
|
||||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
|
||||||
Color.Gray
|
Color.Gray
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Color.Gray
|
Color.Gray
|
||||||
}
|
}
|
||||||
@@ -462,33 +482,29 @@ fun MainScreen(
|
|||||||
val expanded = remember { mutableStateOf(false) }
|
val expanded = remember { mutableStateOf(false) }
|
||||||
RowListItem(
|
RowListItem(
|
||||||
icon = {
|
icon = {
|
||||||
val circleIcon = Icons.Rounded.Circle
|
if (uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
val icon = if (tunnel.isPrimaryTunnel) {
|
Icon(
|
||||||
Icons.Rounded.Star
|
Icons.Rounded.Star,
|
||||||
} else if (tunnel.isMobileDataTunnel) {
|
stringResource(R.string.status),
|
||||||
Icons.Rounded.Smartphone
|
tint = leadingIconColor,
|
||||||
|
modifier = Modifier.padding(end = 10.dp).size(20.dp),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
circleIcon
|
Icon(
|
||||||
|
Icons.Rounded.Circle,
|
||||||
|
stringResource(R.string.status),
|
||||||
|
tint = leadingIconColor,
|
||||||
|
modifier = Modifier.padding(end = 15.dp).size(15.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
icon.name,
|
|
||||||
tint = leadingIconColor,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(
|
|
||||||
end = if (icon == circleIcon) 12.5.dp else 10.dp,
|
|
||||||
start = if (icon == circleIcon) 2.5.dp else 0.dp,
|
|
||||||
)
|
|
||||||
.size(if (icon == circleIcon) 15.dp else 20.dp),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH),
|
text = tunnel.name,
|
||||||
onHold = {
|
onHold = {
|
||||||
if (
|
if (
|
||||||
(uiState.vpnState.status == Tunnel.State.UP) &&
|
(uiState.vpnState.status == Tunnel.State.UP) &&
|
||||||
(tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
(tunnel.name == uiState.vpnState.name)
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||||
return@RowListItem
|
return@RowListItem
|
||||||
}
|
}
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
@@ -498,7 +514,7 @@ fun MainScreen(
|
|||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
(uiState.vpnState.name == tunnel.name)
|
||||||
) {
|
) {
|
||||||
expanded.value = !expanded.value
|
expanded.value = !expanded.value
|
||||||
}
|
}
|
||||||
@@ -512,51 +528,62 @@ fun MainScreen(
|
|||||||
rowButton = {
|
rowButton = {
|
||||||
if (
|
if (
|
||||||
tunnel.id == selectedTunnel?.id &&
|
tunnel.id == selectedTunnel?.id &&
|
||||||
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
|
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (
|
||||||
|
uiState.settings.isAutoTunnelEnabled &&
|
||||||
|
!uiState.settings.isAutoTunnelPaused
|
||||||
|
) {
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showPrimaryChangeAlertDialog = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Star,
|
||||||
|
stringResource(id = R.string.set_primary),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.settings.isAutoTunnelEnabled &&
|
uiState.settings.isAutoTunnelEnabled &&
|
||||||
!uiState.settings.isAutoTunnelPaused
|
uiState.settings.isTunnelConfigDefault(
|
||||||
|
tunnel,
|
||||||
|
) &&
|
||||||
|
!uiState.settings.isAutoTunnelPaused
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
Event.Message.AutoTunnelOffAction.message,
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
)
|
)
|
||||||
} else {
|
} else
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
"${Screen.Option.route}/${selectedTunnel?.id}",
|
"${Screen.Config.route}/${selectedTunnel?.id}",
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
val icon = Icons.Rounded.Settings
|
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
icon.name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.focusable(),
|
|
||||||
onClick = { viewModel.onCopyTunnel(selectedTunnel) },
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.CopyAll
|
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusable(),
|
modifier = Modifier.focusable(),
|
||||||
onClick = { showDeleteTunnelAlertDialog = true },
|
onClick = { showDeleteTunnelAlertDialog = true },
|
||||||
) {
|
) {
|
||||||
val icon = Icons.Rounded.Delete
|
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val checked by remember {
|
val checked by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
(uiState.vpnState.status == Tunnel.State.UP &&
|
(uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
tunnel.name == uiState.vpnState.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!checked) expanded.value = false
|
if (!checked) expanded.value = false
|
||||||
@@ -573,69 +600,77 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Row {
|
Row {
|
||||||
IconButton(
|
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
onClick = {
|
IconButton(
|
||||||
if (uiState.settings.isAutoTunnelEnabled) {
|
onClick = {
|
||||||
appViewModel.showSnackbarMessage(
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
Event.Message.AutoTunnelOffAction.message,
|
showSnackbarMessage(
|
||||||
)
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
} else {
|
)
|
||||||
selectedTunnel = tunnel
|
} else {
|
||||||
navController.navigate(
|
selectedTunnel = tunnel
|
||||||
"${Screen.Option.route}/${selectedTunnel?.id}",
|
showPrimaryChangeAlertDialog = true
|
||||||
)
|
}
|
||||||
}
|
},
|
||||||
},
|
) {
|
||||||
) {
|
Icon(
|
||||||
val icon = Icons.Rounded.Settings
|
Icons.Rounded.Star,
|
||||||
Icon(
|
stringResource(id = R.string.set_primary),
|
||||||
icon,
|
)
|
||||||
icon.name,
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
(uiState.vpnState.name == tunnel.name)
|
||||||
) {
|
) {
|
||||||
expanded.value = !expanded.value
|
expanded.value = !expanded.value
|
||||||
} else {
|
} else {
|
||||||
appViewModel.showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
Event.Message.TunnelOnAction.message,
|
Event.Message.TunnelOnAction.message
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
val icon = Icons.Rounded.Info
|
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = { viewModel.onCopyTunnel(tunnel) },
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.CopyAll
|
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
tunnel.name == uiState.vpnState.tunnelConfig?.name
|
tunnel.name == uiState.vpnState.name
|
||||||
) {
|
) {
|
||||||
appViewModel.showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
Event.Message.TunnelOffAction.message,
|
Event.Message.TunnelOffAction.message
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
navController.navigate(
|
||||||
|
"${Screen.Config.route}/${tunnel.id}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (
|
||||||
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
|
tunnel.name == uiState.vpnState.name
|
||||||
|
) {
|
||||||
|
showSnackbarMessage(
|
||||||
|
Event.Message.TunnelOffAction.message
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
selectedTunnel = tunnel
|
|
||||||
showDeleteTunnelAlertDialog = true
|
showDeleteTunnelAlertDialog = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
val icon = Icons.Rounded.Delete
|
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
Icons.Rounded.Delete,
|
||||||
icon.name,
|
stringResource(id = R.string.delete),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TunnelSwitch()
|
TunnelSwitch()
|
||||||
|
|||||||
+57
-55
@@ -11,7 +11,8 @@ import com.wireguard.config.Config
|
|||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
@@ -20,6 +21,8 @@ import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -35,67 +38,76 @@ class MainViewModel
|
|||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val appDataRepository: AppDataRepository,
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
private val serviceManager: ServiceManager,
|
private val settingsRepository: SettingsRepository,
|
||||||
val vpnService: VpnService
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
combine(
|
combine(
|
||||||
appDataRepository.settings.getSettingsFlow(),
|
settingsRepository.getSettingsFlow(),
|
||||||
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
vpnService.vpnState,
|
vpnService.vpnState,
|
||||||
) { settings, tunnels, vpnState ->
|
) { settings, tunnels, vpnState ->
|
||||||
MainUiState(settings, tunnels, vpnState, false)
|
validateWatcherServiceState(settings)
|
||||||
}
|
MainUiState(settings, tunnels, vpnState, false)
|
||||||
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
MainUiState(),
|
MainUiState(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun validateWatcherServiceState(settings: Settings) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
if (settings.isAutoTunnelEnabled) {
|
||||||
|
ServiceManager.startWatcherService(application.applicationContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun stopWatcherService() =
|
private fun stopWatcherService() =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
serviceManager.stopWatcherService(application.applicationContext)
|
ServiceManager.stopWatcherService(application.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDelete(tunnel: TunnelConfig) {
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val settings = appDataRepository.settings.getSettings()
|
if (tunnelConfigRepository.count() == 1) {
|
||||||
val isPrimary = tunnel.isPrimaryTunnel
|
|
||||||
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
|
|
||||||
stopWatcherService()
|
stopWatcherService()
|
||||||
resetTunnelSetting(settings)
|
val settings = settingsRepository.getSettings()
|
||||||
|
settings.defaultTunnel = null
|
||||||
|
settings.isAutoTunnelEnabled = false
|
||||||
|
settings.isAlwaysOnVpnEnabled = false
|
||||||
|
saveSettings(settings)
|
||||||
}
|
}
|
||||||
appDataRepository.tunnels.delete(tunnel)
|
tunnelConfigRepository.delete(tunnel)
|
||||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resetTunnelSetting(settings: Settings) {
|
|
||||||
saveSettings(
|
|
||||||
settings.copy(
|
|
||||||
isAutoTunnelEnabled = false,
|
|
||||||
isAlwaysOnVpnEnabled = false,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
Timber.d("On start called!")
|
Timber.d("On start called!")
|
||||||
serviceManager.startVpnService(
|
stopActiveTunnel().await()
|
||||||
application.applicationContext,
|
startTunnel(tunnelConfig)
|
||||||
tunnelConfig.id,
|
|
||||||
isManualStart = true,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startTunnel(tunnelConfig: TunnelConfig) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
Timber.d("Start tunnel via manager")
|
||||||
|
ServiceManager.startVpnService(application.applicationContext, tunnelConfig.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopActiveTunnel() =
|
||||||
|
viewModelScope.async(Dispatchers.IO) {
|
||||||
|
onTunnelStop()
|
||||||
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
|
}
|
||||||
|
|
||||||
fun onTunnelStop() =
|
fun onTunnelStop() =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
Timber.i("Stopping active tunnel")
|
Timber.d("Stopping active tunnel")
|
||||||
serviceManager.stopVpnService(application.applicationContext, isManualStop = true)
|
ServiceManager.stopVpnService(application.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateConfigString(config: String) {
|
private fun validateConfigString(config: String) {
|
||||||
@@ -110,7 +122,6 @@ constructor(
|
|||||||
addTunnel(tunnelConfig)
|
addTunnel(tunnelConfig)
|
||||||
Result.Success(Unit)
|
Result.Success(Unit)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
|
||||||
Result.Error(Event.Error.InvalidQrCode)
|
Result.Error(Event.Error.InvalidQrCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,7 +150,6 @@ constructor(
|
|||||||
is Result.Success -> return it
|
is Result.Success -> return it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||||
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
}
|
}
|
||||||
@@ -148,7 +158,6 @@ constructor(
|
|||||||
return Result.Error(Event.Error.InvalidFileExtension)
|
return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
|
||||||
return Result.Error(Event.Error.FileReadFailed)
|
return Result.Error(Event.Error.FileReadFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,25 +190,22 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
val firstTunnel = appDataRepository.tunnels.count() == 0
|
|
||||||
saveTunnel(tunnelConfig)
|
saveTunnel(tunnelConfig)
|
||||||
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pauseAutoTunneling() =
|
fun pauseAutoTunneling() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resumeAutoTunneling() =
|
fun resumeAutoTunneling() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||||
appDataRepository.tunnels.save(tunnelConfig)
|
tunnelConfigRepository.save(tunnelConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
|
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
|
||||||
@@ -243,23 +249,19 @@ constructor(
|
|||||||
return try {
|
return try {
|
||||||
fileName.substring(fileName.lastIndexOf('.'))
|
fileName.substring(fileName.lastIndexOf('.'))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveSettings(settings: Settings) =
|
private fun saveSettings(settings: Settings) =
|
||||||
viewModelScope.launch(Dispatchers.IO) { appDataRepository.settings.save(settings) }
|
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
|
||||||
|
|
||||||
|
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) =
|
||||||
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
tunnel?.let {
|
if (selectedTunnel != null) {
|
||||||
saveTunnel(
|
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
|
||||||
TunnelConfig(
|
.join()
|
||||||
name = it.name.plus(NumberUtils.randomThree()),
|
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||||
wgQuick = it.wgQuick,
|
}
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-287
@@ -1,287 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
|
||||||
|
|
||||||
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.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
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.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
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.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.unit.dp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
|
||||||
fun OptionsScreen(
|
|
||||||
optionsViewModel: OptionsViewModel = hiltViewModel(),
|
|
||||||
navController: NavController,
|
|
||||||
appViewModel: AppViewModel,
|
|
||||||
focusRequester: FocusRequester,
|
|
||||||
tunnelId: String
|
|
||||||
) {
|
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
val uiState by optionsViewModel.uiState.collectAsStateWithLifecycle()
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
val screenPadding = 5.dp
|
|
||||||
val fillMaxWidth = .85f
|
|
||||||
|
|
||||||
var currentText by remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
optionsViewModel.init(tunnelId)
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveTrustedSSID() {
|
|
||||||
if (currentText.isNotEmpty()) {
|
|
||||||
scope.launch {
|
|
||||||
optionsViewModel.onSaveRunSSID(currentText).let {
|
|
||||||
when (it) {
|
|
||||||
is Result.Success -> currentText = ""
|
|
||||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
) {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
tonalElevation = 2.dp,
|
|
||||||
shadowElevation = 2.dp,
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
modifier =
|
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
Modifier
|
|
||||||
.height(IntrinsicSize.Min)
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(top = 10.dp)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(top = 20.dp)
|
|
||||||
})
|
|
||||||
.padding(bottom = 10.dp),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier.padding(15.dp),
|
|
||||||
) {
|
|
||||||
SectionTitle(
|
|
||||||
title = stringResource(id = R.string.general),
|
|
||||||
padding = screenPadding,
|
|
||||||
)
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.set_primary_tunnel),
|
|
||||||
enabled = true,
|
|
||||||
checked = uiState.isDefaultTunnel,
|
|
||||||
modifier = Modifier
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { optionsViewModel.onTogglePrimaryTunnel() },
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(top = 5.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
navController.navigate(
|
|
||||||
"${Screen.Config.route}/${tunnelId}",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.edit_tunnel))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Surface(
|
|
||||||
tonalElevation = 2.dp,
|
|
||||||
shadowElevation = 2.dp,
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
modifier =
|
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
Modifier
|
|
||||||
.height(IntrinsicSize.Min)
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(top = 10.dp)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(top = 20.dp)
|
|
||||||
})
|
|
||||||
.padding(bottom = 10.dp),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier.padding(15.dp),
|
|
||||||
) {
|
|
||||||
SectionTitle(
|
|
||||||
title = stringResource(id = R.string.auto_tunneling),
|
|
||||||
padding = screenPadding,
|
|
||||||
)
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.mobile_data_tunnel),
|
|
||||||
enabled = true,
|
|
||||||
checked = uiState.tunnel?.isMobileDataTunnel == true,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = { optionsViewModel.onToggleIsMobileDataTunnel() },
|
|
||||||
)
|
|
||||||
Column {
|
|
||||||
FlowRow(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(screenPadding)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
|
||||||
) {
|
|
||||||
uiState.tunnel?.tunnelNetworks?.forEach { ssid ->
|
|
||||||
ClickableIconButton(
|
|
||||||
onClick = {
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
optionsViewModel.onDeleteRunSSID(ssid)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onIconClick = {
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
|
|
||||||
optionsViewModel.onDeleteRunSSID(ssid)
|
|
||||||
|
|
||||||
},
|
|
||||||
text = ssid,
|
|
||||||
icon = Icons.Filled.Close,
|
|
||||||
enabled = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (uiState.tunnel == null || uiState.tunnel?.tunnelNetworks?.isEmpty() == true) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.no_wifi_names_configured),
|
|
||||||
fontStyle = FontStyle.Italic,
|
|
||||||
color = Color.Gray,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OutlinedTextField(
|
|
||||||
enabled = true,
|
|
||||||
value = currentText,
|
|
||||||
onValueChange = { currentText = it },
|
|
||||||
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.padding(
|
|
||||||
start = screenPadding,
|
|
||||||
top = 5.dp,
|
|
||||||
bottom = 10.dp,
|
|
||||||
),
|
|
||||||
maxLines = 1,
|
|
||||||
keyboardOptions =
|
|
||||||
KeyboardOptions(
|
|
||||||
capitalization = KeyboardCapitalization.None,
|
|
||||||
imeAction = ImeAction.Done,
|
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
|
||||||
trailingIcon = {
|
|
||||||
if (currentText != "") {
|
|
||||||
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 = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
|
||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
|
|
||||||
data class OptionsUiState(
|
|
||||||
val id: String? = null,
|
|
||||||
val tunnel: TunnelConfig? = null,
|
|
||||||
val isDefaultTunnel: Boolean = false
|
|
||||||
)
|
|
||||||
-101
@@ -1,101 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.options
|
|
||||||
|
|
||||||
import androidx.compose.ui.util.fastFirstOrNull
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class OptionsViewModel @Inject
|
|
||||||
constructor(
|
|
||||||
private val appDataRepository: AppDataRepository
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val _optionState = MutableStateFlow(OptionsUiState())
|
|
||||||
|
|
||||||
val uiState = combine(
|
|
||||||
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
|
||||||
_optionState,
|
|
||||||
) { tunnels, optionState ->
|
|
||||||
if (optionState.id != null) {
|
|
||||||
val tunnelConfig = tunnels.fastFirstOrNull { it.id.toString() == optionState.id }
|
|
||||||
val isPrimaryTunnel = tunnelConfig?.isPrimaryTunnel == true
|
|
||||||
OptionsUiState(optionState.id, tunnelConfig, isPrimaryTunnel)
|
|
||||||
} else OptionsUiState()
|
|
||||||
}.stateIn(
|
|
||||||
viewModelScope,
|
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
|
||||||
OptionsUiState(),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun init(tunnelId: String) {
|
|
||||||
_optionState.value = _optionState.value.copy(
|
|
||||||
id = tunnelId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
uiState.value.tunnel?.let {
|
|
||||||
appDataRepository.tunnels.save(
|
|
||||||
tunnelConfig = it.copy(
|
|
||||||
tunnelNetworks = (uiState.value.tunnel!!.tunnelNetworks - ssid).toMutableList(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveTunnel(tunnelConfig: TunnelConfig?) = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
tunnelConfig?.let {
|
|
||||||
appDataRepository.tunnels.save(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun onSaveRunSSID(ssid: String): Result<Unit> {
|
|
||||||
val trimmed = ssid.trim()
|
|
||||||
val tunnelsWithName = withContext(viewModelScope.coroutineContext) {
|
|
||||||
appDataRepository.tunnels.findByTunnelNetworksName(trimmed)
|
|
||||||
}
|
|
||||||
return if (uiState.value.tunnel?.tunnelNetworks?.contains(trimmed) != true &&
|
|
||||||
tunnelsWithName.isEmpty()) {
|
|
||||||
uiState.value.tunnel?.tunnelNetworks?.add(trimmed)
|
|
||||||
saveTunnel(uiState.value.tunnel)
|
|
||||||
Result.Success(Unit)
|
|
||||||
} else {
|
|
||||||
Result.Error(Event.Error.SsidConflict)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onToggleIsMobileDataTunnel() = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
uiState.value.tunnel?.let {
|
|
||||||
if (it.isMobileDataTunnel) {
|
|
||||||
appDataRepository.tunnels.updateMobileDataTunnel(null)
|
|
||||||
} else appDataRepository.tunnels.updateMobileDataTunnel(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTogglePrimaryTunnel() = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
if (uiState.value.tunnel != null) {
|
|
||||||
appDataRepository.tunnels.updatePrimaryTunnel(
|
|
||||||
when (uiState.value.isDefaultTunnel) {
|
|
||||||
true -> null
|
|
||||||
false -> uiState.value.tunnel
|
|
||||||
},
|
|
||||||
)
|
|
||||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-53
@@ -1,53 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.pinlock
|
|
||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
|
||||||
import xyz.teamgravity.pin_lock_compose.PinLock
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun PinLockScreen(navController: NavController, appViewModel: AppViewModel) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
PinLock(
|
|
||||||
title = { pinExists ->
|
|
||||||
Text(
|
|
||||||
text = if (pinExists) stringResource(id = R.string.enter_pin) else stringResource(
|
|
||||||
id = R.string.create_pin,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
color = MaterialTheme.colorScheme.surface,
|
|
||||||
onPinCorrect = {
|
|
||||||
// pin is correct, navigate or hide pin lock
|
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
navController.navigate(Screen.Main.route)
|
|
||||||
} else {
|
|
||||||
val isPopped = navController.popBackStack()
|
|
||||||
if (!isPopped) {
|
|
||||||
navController.navigate(Screen.Main.route)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
onPinIncorrect = {
|
|
||||||
// pin is incorrect, show error
|
|
||||||
appViewModel.showSnackbarMessage(
|
|
||||||
StringValue.StringResource(R.string.incorrect_pin).asString(context),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onPinCreated = {
|
|
||||||
// pin created for the first time, navigate or hide pin lock
|
|
||||||
appViewModel.showSnackbarMessage(
|
|
||||||
StringValue.StringResource(R.string.pin_created).asString(context),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+126
-164
@@ -20,7 +20,9 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -53,6 +55,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -66,7 +69,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
@@ -74,19 +76,16 @@ import com.wireguard.android.backend.Tunnel
|
|||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
@@ -96,16 +95,15 @@ import java.io.File
|
|||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
viewModel: SettingsViewModel = hiltViewModel(),
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
appViewModel: AppViewModel,
|
padding: PaddingValues,
|
||||||
navController: NavController,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
|
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
@@ -115,14 +113,21 @@ fun SettingsScreen(
|
|||||||
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||||
var didExportFiles by remember { mutableStateOf(false) }
|
var didExportFiles by remember { mutableStateOf(false) }
|
||||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
|
val focusRequester2 = remember { FocusRequester() }
|
||||||
|
|
||||||
val screenPadding = 5.dp
|
val screenPadding = 5.dp
|
||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
|
if (uiState.loading) {
|
||||||
|
LoadingScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val startForResult =
|
val startForResult =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
result: ActivityResult ->
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
result.data
|
val intent = result.data
|
||||||
// Handle the Intent
|
// Handle the Intent
|
||||||
}
|
}
|
||||||
viewModel.setBatteryOptimizeDisableShown()
|
viewModel.setBatteryOptimizeDisableShown()
|
||||||
@@ -136,10 +141,9 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
FileUtils.saveFilesToZip(context, files)
|
FileUtils.saveFilesToZip(context, files)
|
||||||
didExportFiles = true
|
didExportFiles = true
|
||||||
appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message)
|
showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
appViewModel.showSnackbarMessage(Event.Error.Exception(e).message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,9 +163,7 @@ fun SettingsScreen(
|
|||||||
|
|
||||||
fun handleAutoTunnelToggle() {
|
fun handleAutoTunnelToggle() {
|
||||||
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
||||||
if (appViewModel.isRequiredPermissionGranted()) {
|
viewModel.toggleAutoTunnel()
|
||||||
viewModel.onToggleAutoTunnel()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
requestBatteryOptimizationsDisabled()
|
requestBatteryOptimizationsDisabled()
|
||||||
}
|
}
|
||||||
@@ -172,7 +174,7 @@ fun SettingsScreen(
|
|||||||
viewModel.onSaveTrustedSSID(currentText).let {
|
viewModel.onSaveTrustedSSID(currentText).let {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Result.Success -> currentText = ""
|
is Result.Success -> currentText = ""
|
||||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,7 +201,7 @@ fun SettingsScreen(
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
if (
|
if (
|
||||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
||||||
) {
|
) {
|
||||||
checkFineLocationGranted()
|
checkFineLocationGranted()
|
||||||
} else {
|
} else {
|
||||||
@@ -246,16 +248,12 @@ fun SettingsScreen(
|
|||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding),
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(scrollState),
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.LocationOff,
|
Icons.Rounded.LocationOff,
|
||||||
contentDescription = stringResource(id = R.string.map),
|
contentDescription = stringResource(id = R.string.map),
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(30.dp).size(128.dp),
|
||||||
.padding(30.dp)
|
|
||||||
.size(128.dp),
|
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.prominent_background_location_title),
|
stringResource(R.string.prominent_background_location_title),
|
||||||
@@ -271,15 +269,11 @@ fun SettingsScreen(
|
|||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier
|
Modifier.fillMaxWidth().padding(10.dp)
|
||||||
.fillMaxWidth()
|
} else {
|
||||||
.padding(10.dp)
|
Modifier.fillMaxWidth().padding(30.dp)
|
||||||
} else {
|
},
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(30.dp)
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
) {
|
) {
|
||||||
@@ -307,11 +301,11 @@ fun SettingsScreen(
|
|||||||
},
|
},
|
||||||
onError = { _ ->
|
onError = { _ ->
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||||
},
|
},
|
||||||
onFailure = {
|
onFailure = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -320,7 +314,7 @@ fun SettingsScreen(
|
|||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.one_tunnel_required),
|
stringResource(R.string.one_tunnel_required),
|
||||||
@@ -335,10 +329,7 @@ fun SettingsScreen(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
indication = null,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
) {
|
) {
|
||||||
@@ -351,17 +342,14 @@ fun SettingsScreen(
|
|||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier
|
Modifier.height(IntrinsicSize.Min)
|
||||||
.height(IntrinsicSize.Min)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.padding(top = 10.dp)
|
||||||
.padding(top = 10.dp)
|
} else {
|
||||||
} else {
|
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp)
|
||||||
Modifier
|
})
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.padding(bottom = 10.dp),
|
||||||
.padding(top = 20.dp)
|
|
||||||
})
|
|
||||||
.padding(bottom = 10.dp),
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
@@ -375,43 +363,38 @@ fun SettingsScreen(
|
|||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(id = R.string.tunnel_on_wifi),
|
stringResource(id = R.string.tunnel_on_wifi),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
checked = uiState.settings.isTunnelOnWifiEnabled,
|
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
||||||
modifier =
|
modifier =
|
||||||
if (uiState.settings.isAutoTunnelEnabled) Modifier
|
if (uiState.settings.isAutoTunnelEnabled) Modifier
|
||||||
else
|
else
|
||||||
Modifier
|
Modifier.focusRequester(focusRequester).focusProperties {
|
||||||
.focusRequester(focusRequester),
|
down = focusRequester2
|
||||||
|
},
|
||||||
)
|
)
|
||||||
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
||||||
Column {
|
Column {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
|
||||||
.padding(screenPadding)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
) {
|
) {
|
||||||
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
||||||
ClickableIconButton(
|
ClickableIconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
focusRequester.requestFocus()
|
|
||||||
viewModel.onDeleteTrustedSSID(ssid)
|
viewModel.onDeleteTrustedSSID(ssid)
|
||||||
|
focusRequester2.requestFocus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onIconClick = {
|
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
|
|
||||||
viewModel.onDeleteTrustedSSID(ssid)
|
|
||||||
|
|
||||||
},
|
|
||||||
text = ssid,
|
text = ssid,
|
||||||
icon = Icons.Filled.Close,
|
icon = Icons.Filled.Close,
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
||||||
@@ -424,24 +407,24 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
value = currentText,
|
value = currentText,
|
||||||
onValueChange = { currentText = it },
|
onValueChange = { currentText = it },
|
||||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.padding(
|
||||||
.padding(
|
start = screenPadding,
|
||||||
start = screenPadding,
|
top = 5.dp,
|
||||||
top = 5.dp,
|
bottom = 10.dp,
|
||||||
bottom = 10.dp,
|
)
|
||||||
),
|
.focusRequester(focusRequester2),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
keyboardOptions =
|
keyboardOptions =
|
||||||
KeyboardOptions(
|
KeyboardOptions(
|
||||||
capitalization = KeyboardCapitalization.None,
|
capitalization = KeyboardCapitalization.None,
|
||||||
imeAction = ImeAction.Done,
|
imeAction = ImeAction.Done,
|
||||||
),
|
),
|
||||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (currentText != "") {
|
if (currentText != "") {
|
||||||
@@ -449,19 +432,19 @@ fun SettingsScreen(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Add,
|
imageVector = Icons.Outlined.Add,
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (currentText == "") {
|
if (currentText == "") {
|
||||||
stringResource(
|
stringResource(
|
||||||
id =
|
id =
|
||||||
R.string
|
R.string
|
||||||
.trusted_ssid_empty_description,
|
.trusted_ssid_empty_description,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
stringResource(
|
stringResource(
|
||||||
id =
|
id =
|
||||||
R.string
|
R.string
|
||||||
.trusted_ssid_value_description,
|
.trusted_ssid_value_description,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -473,8 +456,8 @@ fun SettingsScreen(
|
|||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.tunnel_mobile_data),
|
stringResource(R.string.tunnel_mobile_data),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
||||||
@@ -482,31 +465,31 @@ fun SettingsScreen(
|
|||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(id = R.string.tunnel_on_ethernet),
|
stringResource(id = R.string.tunnel_on_ethernet),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
|
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
|
||||||
)
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.restart_on_ping),
|
stringResource(R.string.battery_saver),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
checked = uiState.settings.isPingEnabled,
|
checked = uiState.settings.isBatterySaverEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleRestartOnPing() },
|
onCheckChanged = { viewModel.onToggleBatterySaver() },
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier =
|
modifier =
|
||||||
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
||||||
else
|
else
|
||||||
Modifier.focusRequester(
|
Modifier.focusRequester(
|
||||||
focusRequester,
|
focusRequester,
|
||||||
))
|
))
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(top = 5.dp),
|
.padding(top = 5.dp),
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -514,22 +497,19 @@ fun SettingsScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.settings.isTunnelOnWifiEnabled &&
|
uiState.settings.isTunnelOnWifiEnabled &&
|
||||||
!uiState.settings.isAutoTunnelEnabled
|
!uiState.settings.isAutoTunnelEnabled
|
||||||
) {
|
) {
|
||||||
when (false) {
|
when (false) {
|
||||||
isBackgroundLocationGranted ->
|
isBackgroundLocationGranted ->
|
||||||
appViewModel.showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
Event.Error.BackgroundLocationRequired.message,
|
Event.Error.BackgroundLocationRequired.message
|
||||||
)
|
)
|
||||||
|
|
||||||
fineLocationState.status.isGranted ->
|
fineLocationState.status.isGranted ->
|
||||||
appViewModel.showSnackbarMessage(
|
showSnackbarMessage(
|
||||||
Event.Error.PreciseLocationRequired.message,
|
Event.Error.PreciseLocationRequired.message
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModel.isLocationEnabled(context) ->
|
viewModel.isLocationEnabled(context) ->
|
||||||
showLocationServicesAlertDialog = true
|
showLocationServicesAlertDialog = true
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
handleAutoTunnelToggle()
|
handleAutoTunnelToggle()
|
||||||
}
|
}
|
||||||
@@ -556,9 +536,7 @@ fun SettingsScreen(
|
|||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp),
|
||||||
.fillMaxWidth(fillMaxWidth)
|
|
||||||
.padding(vertical = 10.dp),
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
@@ -572,15 +550,15 @@ fun SettingsScreen(
|
|||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.use_kernel),
|
stringResource(R.string.use_kernel),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||||
(uiState.vpnState.status == Tunnel.State.UP)),
|
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||||
checked = uiState.settings.isKernelEnabled,
|
checked = uiState.settings.isKernelEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = {
|
||||||
viewModel.onToggleKernelMode().let {
|
viewModel.onToggleKernelMode().let {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||||
is Result.Success -> {}
|
is Result.Success -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -589,27 +567,26 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Surface(
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
tonalElevation = 2.dp,
|
Surface(
|
||||||
shadowElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shadowElevation = 2.dp,
|
||||||
color = MaterialTheme.colorScheme.surface,
|
shape = RoundedCornerShape(12.dp),
|
||||||
modifier =
|
color = MaterialTheme.colorScheme.surface,
|
||||||
Modifier
|
modifier =
|
||||||
.fillMaxWidth(fillMaxWidth)
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
.padding(vertical = 10.dp)
|
.padding(vertical = 10.dp)
|
||||||
.padding(bottom = 140.dp),
|
.padding(bottom = 140.dp),
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier.padding(15.dp),
|
|
||||||
) {
|
) {
|
||||||
SectionTitle(
|
Column(
|
||||||
title = stringResource(id = R.string.other),
|
horizontalAlignment = Alignment.Start,
|
||||||
padding = screenPadding,
|
verticalArrangement = Arrangement.Top,
|
||||||
)
|
modifier = Modifier.padding(15.dp),
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
) {
|
||||||
|
SectionTitle(
|
||||||
|
title = stringResource(id = R.string.other),
|
||||||
|
padding = screenPadding,
|
||||||
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.always_on_vpn_support),
|
stringResource(R.string.always_on_vpn_support),
|
||||||
enabled = !uiState.settings.isAutoTunnelEnabled,
|
enabled = !uiState.settings.isAutoTunnelEnabled,
|
||||||
@@ -624,27 +601,9 @@ fun SettingsScreen(
|
|||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
||||||
)
|
)
|
||||||
}
|
|
||||||
ConfigurationToggle(
|
|
||||||
stringResource(R.string.enable_app_lock),
|
|
||||||
enabled = true,
|
|
||||||
checked = pinExists.value,
|
|
||||||
padding = screenPadding,
|
|
||||||
onCheckChanged = {
|
|
||||||
if (pinExists.value) {
|
|
||||||
PinManager.clearPin()
|
|
||||||
pinExists.value = PinManager.pinExists()
|
|
||||||
} else {
|
|
||||||
navController.navigate(Screen.Lock.route)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(top = 5.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -657,6 +616,9 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
Spacer(modifier = Modifier.weight(.17f))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -9,5 +9,6 @@ data class SettingsUiState(
|
|||||||
val tunnels: List<TunnelConfig> = emptyList(),
|
val tunnels: List<TunnelConfig> = emptyList(),
|
||||||
val vpnState: VpnState = VpnState(),
|
val vpnState: VpnState = VpnState(),
|
||||||
val isLocationDisclosureShown: Boolean = true,
|
val isLocationDisclosureShown: Boolean = true,
|
||||||
val isBatteryOptimizeDisableShown: Boolean = false
|
val isBatteryOptimizeDisableShown: Boolean = false,
|
||||||
|
val loading: Boolean = true
|
||||||
)
|
)
|
||||||
|
|||||||
+47
-39
@@ -7,9 +7,10 @@ import androidx.core.location.LocationManagerCompat
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
@@ -28,27 +29,29 @@ class SettingsViewModel
|
|||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val appDataRepository: AppDataRepository,
|
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||||
private val serviceManager: ServiceManager,
|
private val settingsRepository: SettingsRepository,
|
||||||
|
private val dataStoreManager: DataStoreManager,
|
||||||
private val rootShell: RootShell,
|
private val rootShell: RootShell,
|
||||||
vpnService: VpnService
|
private val vpnService: VpnService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
combine(
|
combine(
|
||||||
appDataRepository.settings.getSettingsFlow(),
|
settingsRepository.getSettingsFlow(),
|
||||||
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
vpnService.vpnState,
|
vpnService.vpnState,
|
||||||
appDataRepository.appState.generalStateFlow,
|
dataStoreManager.preferencesFlow,
|
||||||
) { settings, tunnels, tunnelState, generalState ->
|
) { settings, tunnels, tunnelState, preferences ->
|
||||||
SettingsUiState(
|
SettingsUiState(
|
||||||
settings,
|
settings,
|
||||||
tunnels,
|
tunnels,
|
||||||
tunnelState,
|
tunnelState,
|
||||||
generalState.locationDisclosureShown,
|
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
|
||||||
generalState.batteryOptimizationDisableShown,
|
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
|
||||||
)
|
false
|
||||||
}
|
)
|
||||||
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
@@ -68,12 +71,12 @@ constructor(
|
|||||||
|
|
||||||
fun setLocationDisclosureShown() =
|
fun setLocationDisclosureShown() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
appDataRepository.appState.setLocationDisclosureShown(true)
|
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setBatteryOptimizeDisableShown() =
|
fun setBatteryOptimizeDisableShown() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
|
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleTunnelOnMobileData() {
|
fun onToggleTunnelOnMobileData() {
|
||||||
@@ -88,42 +91,48 @@ constructor(
|
|||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
trustedNetworkSSIDs =
|
trustedNetworkSSIDs =
|
||||||
(uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(),
|
(uiState.value.settings.trustedNetworkSSIDs - ssid).toMutableList(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleAutoTunnel() =
|
private suspend fun getDefaultTunnelOrFirst(): String {
|
||||||
|
return uiState.value.settings.defaultTunnel
|
||||||
|
?: tunnelConfigRepository.getAll().first().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleAutoTunnel() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
||||||
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
||||||
|
|
||||||
if (isAutoTunnelEnabled) {
|
if (isAutoTunnelEnabled) {
|
||||||
serviceManager.stopWatcherService(application)
|
ServiceManager.stopWatcherService(application)
|
||||||
} else {
|
} else {
|
||||||
serviceManager.startWatcherService(application)
|
ServiceManager.startWatcherService(application)
|
||||||
isAutoTunnelPaused = false
|
isAutoTunnelPaused = false
|
||||||
}
|
}
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
||||||
isAutoTunnelPaused = isAutoTunnelPaused,
|
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||||
|
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleAlwaysOnVPN() =
|
fun onToggleAlwaysOnVPN() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
saveSettings(
|
val updatedSettings =
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
||||||
),
|
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||||
)
|
)
|
||||||
|
saveSettings(updatedSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveSettings(settings: Settings) =
|
private fun saveSettings(settings: Settings) =
|
||||||
viewModelScope.launch { appDataRepository.settings.save(settings) }
|
viewModelScope.launch { settingsRepository.save(settings) }
|
||||||
|
|
||||||
fun onToggleTunnelOnEthernet() {
|
fun onToggleTunnelOnEthernet() {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
@@ -146,6 +155,14 @@ constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onToggleBatterySaver() {
|
||||||
|
saveSettings(
|
||||||
|
uiState.value.settings.copy(
|
||||||
|
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun saveKernelMode(on: Boolean) {
|
private fun saveKernelMode(on: Boolean) {
|
||||||
saveSettings(
|
saveSettings(
|
||||||
uiState.value.settings.copy(
|
uiState.value.settings.copy(
|
||||||
@@ -166,10 +183,9 @@ constructor(
|
|||||||
if (!uiState.value.settings.isKernelEnabled) {
|
if (!uiState.value.settings.isKernelEnabled) {
|
||||||
try {
|
try {
|
||||||
rootShell.start()
|
rootShell.start()
|
||||||
Timber.i("Root shell accepted!")
|
Timber.d("Root shell accepted!")
|
||||||
saveKernelMode(on = true)
|
saveKernelMode(on = true)
|
||||||
} catch (e: RootShell.RootShellException) {
|
} catch (e: RootShell.RootShellException) {
|
||||||
Timber.e(e)
|
|
||||||
saveKernelMode(on = false)
|
saveKernelMode(on = false)
|
||||||
return Result.Error(Event.Error.RootDenied)
|
return Result.Error(Event.Error.RootDenied)
|
||||||
}
|
}
|
||||||
@@ -178,12 +194,4 @@ constructor(
|
|||||||
}
|
}
|
||||||
return Result.Success(Unit)
|
return Result.Success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleRestartOnPing() = viewModelScope.launch {
|
|
||||||
saveSettings(
|
|
||||||
uiState.value.settings.copy(
|
|
||||||
isPingEnabled = !uiState.value.settings.isPingEnabled,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-103
@@ -1,10 +1,14 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.Intent.createChooser
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -16,11 +20,10 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowForward
|
import androidx.compose.material.icons.rounded.ArrowForward
|
||||||
import androidx.compose.material.icons.rounded.Book
|
import androidx.compose.material.icons.rounded.Book
|
||||||
import androidx.compose.material.icons.rounded.FormatListNumbered
|
|
||||||
import androidx.compose.material.icons.rounded.Mail
|
import androidx.compose.material.icons.rounded.Mail
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.Divider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@@ -42,20 +45,21 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SupportScreen(
|
fun SupportScreen(
|
||||||
viewModel: SupportViewModel = hiltViewModel(),
|
viewModel: SupportViewModel = hiltViewModel(),
|
||||||
appViewModel: AppViewModel,
|
padding: PaddingValues,
|
||||||
navController: NavController,
|
showSnackbarMessage: (String) -> Unit,
|
||||||
focusRequester: FocusRequester
|
focusRequester: FocusRequester
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -63,14 +67,47 @@ fun SupportScreen(
|
|||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
fun openWebPage(url: String) {
|
||||||
|
try {
|
||||||
|
val webpage: Uri = Uri.parse(url)
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchEmail() {
|
||||||
|
try {
|
||||||
|
val intent =
|
||||||
|
Intent(Intent.ACTION_SENDTO).apply {
|
||||||
|
type = Constants.EMAIL_MIME_TYPE
|
||||||
|
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||||
|
}
|
||||||
|
startActivity(
|
||||||
|
context,
|
||||||
|
createChooser(intent, context.getString(R.string.email_chooser)),
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.loading) {
|
||||||
|
LoadingScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxSize()
|
||||||
.fillMaxSize()
|
.verticalScroll(rememberScrollState())
|
||||||
.verticalScroll(rememberScrollState())
|
.focusable()
|
||||||
.focusable(),
|
.padding(padding),
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
@@ -78,20 +115,16 @@ fun SupportScreen(
|
|||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier
|
Modifier.height(IntrinsicSize.Min)
|
||||||
.height(IntrinsicSize.Min)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.padding(top = 10.dp)
|
||||||
.padding(top = 10.dp)
|
} else {
|
||||||
} else {
|
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
||||||
Modifier
|
})
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.padding(bottom = 25.dp),
|
||||||
.padding(top = 20.dp)
|
|
||||||
})
|
|
||||||
.padding(bottom = 25.dp),
|
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(20.dp)) {
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward
|
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.thank_you),
|
stringResource(R.string.thank_you),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
@@ -106,10 +139,8 @@ fun SupportScreen(
|
|||||||
modifier = Modifier.padding(bottom = 20.dp),
|
modifier = Modifier.padding(bottom = 20.dp),
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) },
|
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester),
|
||||||
.padding(vertical = 5.dp)
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@@ -117,26 +148,19 @@ fun SupportScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
val icon = Icons.Rounded.Book
|
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
||||||
Icon(icon, icon.name)
|
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.docs_description),
|
stringResource(id = R.string.docs_description),
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp),
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
forwardIcon,
|
|
||||||
forwardIcon.name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
thickness = 0.5.dp,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
)
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) },
|
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||||
modifier = Modifier.padding(vertical = 5.dp),
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -145,10 +169,9 @@ fun SupportScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
val icon = ImageVector.vectorResource(R.drawable.discord)
|
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
||||||
icon.name,
|
stringResource(id = R.string.discord),
|
||||||
Modifier.size(25.dp),
|
Modifier.size(25.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
@@ -157,18 +180,12 @@ fun SupportScreen(
|
|||||||
modifier = Modifier.padding(start = 10.dp),
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
forwardIcon,
|
|
||||||
forwardIcon.name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
thickness = 0.5.dp,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
)
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) },
|
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
||||||
modifier = Modifier.padding(vertical = 5.dp),
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -177,30 +194,23 @@ fun SupportScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
val icon = ImageVector.vectorResource(R.drawable.github)
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = ImageVector.vectorResource(R.drawable.github),
|
||||||
icon.name,
|
stringResource(id = R.string.github),
|
||||||
Modifier.size(25.dp),
|
Modifier.size(25.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.open_issue),
|
"Open an issue",
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp),
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
forwardIcon,
|
|
||||||
forwardIcon.name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(
|
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||||
thickness = 0.5.dp,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
)
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { appViewModel.launchEmail() },
|
onClick = { launchEmail() },
|
||||||
modifier = Modifier.padding(vertical = 5.dp),
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -209,48 +219,14 @@ fun SupportScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
val icon = Icons.Rounded.Mail
|
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
||||||
Icon(icon, icon.name)
|
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.email_description),
|
stringResource(id = R.string.email_description),
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp),
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||||
forwardIcon,
|
|
||||||
forwardIcon.name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
HorizontalDivider(
|
|
||||||
thickness = 0.5.dp,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
)
|
|
||||||
TextButton(
|
|
||||||
onClick = { navController.navigate(Screen.Support.Logs.route) },
|
|
||||||
modifier = Modifier.padding(vertical = 5.dp),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Row {
|
|
||||||
val icon = Icons.Rounded.FormatListNumbered
|
|
||||||
Icon(icon, icon.name)
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.read_logs),
|
|
||||||
textAlign = TextAlign.Justify,
|
|
||||||
modifier = Modifier.padding(start = 10.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Rounded.ArrowForward,
|
|
||||||
stringResource(id = R.string.go),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,9 +237,9 @@ fun SupportScreen(
|
|||||||
style = TextStyle(textDecoration = TextDecoration.Underline),
|
style = TextStyle(textDecoration = TextDecoration.Underline),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.clickable {
|
Modifier.clickable {
|
||||||
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(25.dp),
|
horizontalArrangement = Arrangement.spacedBy(25.dp),
|
||||||
|
|||||||
+1
-1
@@ -2,4 +2,4 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
|||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
|
||||||
data class SupportUiState(val settings: Settings = Settings())
|
data class SupportUiState(val settings: Settings = Settings(), val loading: Boolean = true)
|
||||||
|
|||||||
+2
-2
@@ -11,13 +11,13 @@ import kotlinx.coroutines.flow.stateIn
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SupportViewModel @Inject constructor(settingsRepository: SettingsRepository) :
|
class SupportViewModel @Inject constructor(private val settingsRepository: SettingsRepository) :
|
||||||
ViewModel() {
|
ViewModel() {
|
||||||
|
|
||||||
val uiState =
|
val uiState =
|
||||||
settingsRepository
|
settingsRepository
|
||||||
.getSettingsFlow()
|
.getSettingsFlow()
|
||||||
.map { SupportUiState(it) }
|
.map { SupportUiState(it, false) }
|
||||||
.stateIn(
|
.stateIn(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
|
|||||||
-110
@@ -1,110 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Save
|
|
||||||
import androidx.compose.material3.FloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
|
||||||
@Composable
|
|
||||||
fun LogsScreen(appViewModel: AppViewModel) {
|
|
||||||
|
|
||||||
val logs = remember {
|
|
||||||
appViewModel.logs
|
|
||||||
}
|
|
||||||
|
|
||||||
val lazyColumnListState = rememberLazyListState()
|
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
LaunchedEffect(logs.size) {
|
|
||||||
scope.launch {
|
|
||||||
lazyColumnListState.animateScrollToItem(logs.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
floatingActionButton = {
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = {
|
|
||||||
appViewModel.saveLogsToFile()
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
) {
|
|
||||||
val icon = Icons.Filled.Save
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = icon.name,
|
|
||||||
tint = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
LazyColumn(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
|
|
||||||
state = lazyColumnListState,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 24.dp),
|
|
||||||
) {
|
|
||||||
items(logs) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start),
|
|
||||||
verticalAlignment = Alignment.Top,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = null,
|
|
||||||
onClick = {
|
|
||||||
clipboardManager.setText(annotatedString = AnnotatedString(it.toString()))
|
|
||||||
},
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
val fontSize = 10.sp
|
|
||||||
Text(text = it.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize)
|
|
||||||
LogTypeLabel(color = Color(it.level.color())) {
|
|
||||||
Text(
|
|
||||||
text = it.level.signifier,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
fontSize = fontSize,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Text("${it.message} - ${it.time}", fontSize = fontSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -57,7 +57,6 @@ fun WireguardAutoTunnelTheme(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
darkTheme -> DarkColorScheme
|
||||||
else -> LightColorScheme
|
else -> LightColorScheme
|
||||||
}
|
}
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,13 +10,13 @@ import androidx.compose.ui.unit.sp
|
|||||||
val Typography =
|
val Typography =
|
||||||
Typography(
|
Typography(
|
||||||
bodyLarge =
|
bodyLarge =
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
lineHeight = 24.sp,
|
lineHeight = 24.sp,
|
||||||
letterSpacing = 0.5.sp,
|
letterSpacing = 0.5.sp,
|
||||||
),
|
),
|
||||||
/* Other default text styles to override
|
/* Other default text styles to override
|
||||||
titleLarge = TextStyle(
|
titleLarge = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
|
|||||||
@@ -1,38 +1,22 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
|
|
||||||
const val BASE_LOG_FILE_NAME = "wgtunnel-logs"
|
|
||||||
const val LOG_BUFFER_SIZE = 3_000L
|
|
||||||
|
|
||||||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||||
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
|
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes
|
||||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
|
const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1000L // 30 minutes
|
||||||
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
|
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
||||||
|
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3000L
|
||||||
const val TOGGLE_TUNNEL_DELAY = 300L
|
const val TOGGLE_TUNNEL_DELAY = 300L
|
||||||
const val WATCHER_COLLECTION_DELAY = 1_000L
|
|
||||||
const val CONF_FILE_EXTENSION = ".conf"
|
const val CONF_FILE_EXTENSION = ".conf"
|
||||||
const val ZIP_FILE_EXTENSION = ".zip"
|
const val ZIP_FILE_EXTENSION = ".zip"
|
||||||
const val URI_CONTENT_SCHEME = "content"
|
const val URI_CONTENT_SCHEME = "content"
|
||||||
|
const val URI_PACKAGE_SCHEME = "package"
|
||||||
const val ALLOWED_FILE_TYPES = "*/*"
|
const val ALLOWED_FILE_TYPES = "*/*"
|
||||||
const val TEXT_MIME_TYPE = "text/plain"
|
|
||||||
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
||||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||||
const val ALWAYS_ON_VPN_ACTION = "android.net.VpnService"
|
|
||||||
const val EMAIL_MIME_TYPE = "message/rfc822"
|
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||||
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
||||||
|
|
||||||
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
||||||
const val FOCUS_REQUEST_DELAY = 500L
|
const val FOCUS_REQUEST_DELAY = 500L
|
||||||
|
|
||||||
const val BACKUP_PING_HOST = "1.1.1.1"
|
|
||||||
const val PING_TIMEOUT = 5_000L
|
|
||||||
const val VPN_RESTART_DELAY = 1_000L
|
|
||||||
const val PING_INTERVAL = 60_000L
|
|
||||||
const val PING_COOLDOWN = PING_INTERVAL * 60 //one hour
|
|
||||||
|
|
||||||
const val ALLOWED_DISPLAY_NAME_LENGTH = 20
|
|
||||||
|
|
||||||
const val TUNNEL_EXTRA_KEY = "tunnelId"
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,6 @@ sealed class Event {
|
|||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ConfigParseError(val appendedMessage: String) : Error() {
|
|
||||||
override val message: String =
|
|
||||||
WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + (
|
|
||||||
if (appendedMessage != "") ": ${appendedMessage.trim()}" else "")
|
|
||||||
}
|
|
||||||
|
|
||||||
data object RootDenied : Error() {
|
data object RootDenied : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
||||||
|
|||||||
@@ -31,12 +31,6 @@ fun BroadcastReceiver.goAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.truncateWithEllipsis(allowedLength: Int): String {
|
|
||||||
return if (this.length > allowedLength + 3) {
|
|
||||||
this.substring(0, allowedLength) + "***"
|
|
||||||
} else this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||||
val df = DecimalFormat("#.###")
|
val df = DecimalFormat("#.###")
|
||||||
return df.format(this)
|
return df.format(this)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.os.Environment
|
|||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.MediaStore.MediaColumns
|
import android.provider.MediaStore.MediaColumns
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
@@ -16,7 +15,6 @@ import java.util.zip.ZipOutputStream
|
|||||||
object FileUtils {
|
object FileUtils {
|
||||||
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
||||||
|
|
||||||
//TODO issue with android 9
|
|
||||||
private fun createDownloadsFileOutputStream(
|
private fun createDownloadsFileOutputStream(
|
||||||
context: Context,
|
context: Context,
|
||||||
fileName: String,
|
fileName: String,
|
||||||
@@ -45,31 +43,6 @@ object FileUtils {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveFileToDownloads(context: Context, content: String, fileName: String) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
val contentValues = ContentValues().apply {
|
|
||||||
put(MediaColumns.DISPLAY_NAME, fileName)
|
|
||||||
put(MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
|
|
||||||
put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
|
||||||
}
|
|
||||||
val resolver = context.contentResolver
|
|
||||||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
|
||||||
if (uri != null) {
|
|
||||||
resolver.openOutputStream(uri).use { output ->
|
|
||||||
output?.write(content.toByteArray())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val target = File(
|
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
fileName,
|
|
||||||
)
|
|
||||||
FileOutputStream(target).use { output ->
|
|
||||||
output.write(content.toByteArray())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveFilesToZip(context: Context, files: List<File>) {
|
fun saveFilesToZip(context: Context, files: List<File>) {
|
||||||
val zipOutputStream =
|
val zipOutputStream =
|
||||||
createDownloadsFileOutputStream(
|
createDownloadsFileOutputStream(
|
||||||
|
|||||||
@@ -19,15 +19,7 @@ object NumberUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun generateRandomTunnelName(): String {
|
fun generateRandomTunnelName(): String {
|
||||||
return "tunnel${randomFive()}"
|
return "tunnel${(Math.random() * 100000).toInt()}"
|
||||||
}
|
|
||||||
|
|
||||||
private fun randomFive(): Int {
|
|
||||||
return (Math.random() * 100000).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun randomThree(): Int {
|
|
||||||
return (Math.random() * 1000).toInt()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSecondsBetweenTimestampAndNow(epoch: Long): Long? {
|
fun getSecondsBetweenTimestampAndNow(epoch: Long): Long? {
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
|
||||||
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class ReleaseTree : Timber.DebugTree() {
|
|
||||||
override fun d(t: Throwable?) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun d(t: Throwable?, message: String?, vararg args: Any?) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun d(message: String?, vararg args: Any?) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
|
|
||||||
sealed class StringValue {
|
|
||||||
|
|
||||||
data class DynamicString(val value: String) : StringValue()
|
|
||||||
|
|
||||||
data object Empty : StringValue()
|
|
||||||
|
|
||||||
class StringResource(
|
|
||||||
@StringRes val resId: Int,
|
|
||||||
vararg val args: Any
|
|
||||||
) : StringValue()
|
|
||||||
|
|
||||||
fun asString(context: Context?): String {
|
|
||||||
return when (this) {
|
|
||||||
is Empty -> ""
|
|
||||||
is DynamicString -> value
|
|
||||||
is StringResource -> context?.getString(resId, *args).orEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="#FFFFFF"
|
|
||||||
android:viewportWidth="640"
|
|
||||||
android:viewportHeight="640">
|
|
||||||
<group
|
|
||||||
android:scaleX="1.0132159"
|
|
||||||
android:scaleY="1.0132159"
|
|
||||||
android:translateX="-4.229075"
|
|
||||||
android:translateY="-4.229075">
|
|
||||||
<path
|
|
||||||
android:fillColor="#53bdb6"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:pathData="M316.72,80.15C314.94,80.82 312.08,83.95 308.79,88.84C275.66,138.15 157.88,161.96 119.66,127.08C109.97,118.24 101.21,118.84 98.97,128.5C96.01,141.29 98.49,204.07 103.12,233.5C123.71,364.32 186.77,465.69 303.03,554.88C314.06,563.34 316.63,563.42 326.93,555.61C329.91,553.35 336.21,548.63 340.93,545.13C345.64,541.63 350.87,537.46 352.55,535.88C354.23,534.29 357.92,531.53 360.75,529.74C413.56,496.43 481.74,399.04 510.38,316C527.22,267.19 534.86,236.66 539.96,197.9C547.99,136.74 545.31,124.46 526,134.01C469.85,161.74 361.02,137.16 333.39,90.49C327.63,80.75 322.93,77.84 316.72,80.15M307.5,195.34C282.24,203.76 266.16,237.38 269.85,274.04C270.99,285.39 271.18,285 264.63,285C254.13,285 254.15,284.87 255,352.05C255.64,402.94 250.1,397.02 298.5,398.52C352.54,400.2 377.63,400.23 379.23,398.63C381.67,396.18 381.86,392.86 382.46,339.89C383.09,283.93 383.39,286 374.51,286C367.07,286 367.21,286.26 367.77,273.58C369.89,225.38 338.74,184.92 307.5,195.34M308.93,216.98C294.31,224.7 281.68,270.05 290.33,283.75C291.74,285.99 346.85,285.51 347.78,283.25C359.48,254.75 330.62,205.51 308.93,216.98M309.54,317.1C304.18,321.81 304.39,327.76 310.11,332.99C314.78,337.26 314.82,336.51 309.33,349.89C307.44,354.51 306.13,358.89 306.42,359.64C306.96,361.06 329,361.75 329,360.35C329,359.98 327.42,354.82 325.5,348.86C323.58,342.91 322,337.52 322,336.89C322,336.26 323.58,334 325.5,331.87C335.69,320.59 321.02,307.02 309.54,317.1"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 544 B |
Binary file not shown.
|
Before Width: | Height: | Size: 382 B |
Binary file not shown.
|
Before Width: | Height: | Size: 698 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:viewportHeight="960">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M360,600v-240h80v240h-80ZM520,600v-240h80v240h-80ZM480,920q-108,0 -202.5,-49.5T120,732v108L40,840v-240h240v80h-98q51,75 129.5,117.5T480,840q115,0 208.5,-66T820,599l78,18q-45,136 -160,219.5T480,920ZM42,440q7,-67 32,-128.5T143,198l57,57q-32,41 -52,87.5T123,440L42,440ZM256,199 L199,142q53,-44 114,-69.5T440,42v80q-51,5 -97,25t-87,52ZM705,199q-41,-32 -87.5,-52T520,122v-80q67,6 128.5,31T762,142l-57,57ZM838,440q-5,-51 -25,-97.5T761,255l57,-57q44,52 69,113.5T918,440h-80Z" />
|
|
||||||
</vector>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user