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: Android CI Tag Deployment (Pre-release)
|
||||
name: Android CI Tag Deployment
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*-**'
|
||||
- '*.*.*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -14,13 +13,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
KEY_STORE_PATH: ${{ secrets.KEY_STORE_PATH }}
|
||||
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
|
||||
@@ -39,16 +35,10 @@ jobs:
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
with:
|
||||
fileName: ${{ env.KEY_STORE_FILE }}
|
||||
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||
fileName: 'android_keystore.jks'
|
||||
fileDir: ${{ github.workspace }}/app/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
|
||||
id: createServiceAccount
|
||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||
@@ -62,13 +52,10 @@ jobs:
|
||||
- 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
|
||||
uses: actions/upload-artifact@v4.0.0
|
||||
with:
|
||||
name: wgtunnel
|
||||
path: ${{ steps.apk-path.outputs.path }}
|
||||
@@ -78,46 +65,23 @@ jobs:
|
||||
name: wgtunnel
|
||||
- name: Create Release with Fastlane changelog notes
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
# 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 }}
|
||||
name: ${{ github.ref_name }}
|
||||
name: Release ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
prerelease: false
|
||||
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||
|
||||
- name: Install apksigner
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y apksigner
|
||||
|
||||
- name: Get checksum
|
||||
id: checksum
|
||||
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*")" | awk '{$1=$1};1' >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Append checksum
|
||||
id: append_checksum
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body: >
|
||||
<br /> SHA256 Checksum: <br /> ```${{ steps.checksum.outputs.checksum }}```
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
append_body: true
|
||||
|
||||
- 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 Beta track 🚀
|
||||
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
||||
|
||||
@@ -1,129 +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: Install apksigner
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y apksigner
|
||||
|
||||
- name: Get checksum
|
||||
id: checksum
|
||||
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*")" | awk '{$1=$1};1' >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Append checksum
|
||||
id: append_checksum
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body: >
|
||||
<br /> SHA256 Checksum: <br /> ```${{ steps.checksum.outputs.checksum }}```
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
append_body: true
|
||||
|
||||
- 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://www.amazon.com/gp/product/B0CFGGL7WK)
|
||||
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://ko-fi.com/N4N8NMJN2)
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -49,23 +56,20 @@ and on while on different networks. This app was created to offer a free solutio
|
||||
## Features
|
||||
|
||||
* Add tunnels via .conf file, zip, manual entry, or QR code
|
||||
* Auto connect to tunnels based on Wi-Fi SSID, ethernet, or mobile data
|
||||
* Auto connect to VPN based on Wi-Fi SSID, ethernet, or mobile data
|
||||
* Split tunneling by application with search
|
||||
* WireGuard support for kernel and userspace modes
|
||||
* Always-On VPN support
|
||||
* Export tunnels to zip
|
||||
* Quick tile support for tunnel toggling, auto-tunneling
|
||||
* Static shortcuts support for tunnel toggling, auto-tunneling
|
||||
* Quick tile support for VPN toggling
|
||||
* Static shortcuts support for primary tunnel for automation integration
|
||||
* Intent automation support for all tunnels
|
||||
* Automatic auto-tunneling service restart after reboot
|
||||
* Automatic tunnel restart after reboot
|
||||
* Automatic service restart after reboot
|
||||
* Battery preservation measures
|
||||
* Restart tunnel on ping failure (beta)
|
||||
|
||||
## Docs (WIP)
|
||||
|
||||
Basic documentation of the feature and behaviors of this app can be
|
||||
found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
||||
Basic documentation of the feature and behaviors of this app can be found [here](https://zaneschepke.com/wgtunnel-docs/overview.html).
|
||||
|
||||
The repository for these docs can be found [here](https://github.com/zaneschepke/wgtunnel-docs).
|
||||
|
||||
@@ -74,11 +78,6 @@ The repository for these docs can be found [here](https://github.com/zaneschepke
|
||||
```
|
||||
$ git clone https://github.com/zaneschepke/wgtunnel
|
||||
$ cd wgtunnel
|
||||
```
|
||||
|
||||
And then build the app:
|
||||
|
||||
```
|
||||
$ ./gradlew assembleDebug
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues to `support@zaneschepke.com`
|
||||
+11
-9
@@ -4,7 +4,7 @@ plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.hilt.android)
|
||||
alias(libs.plugins.kotlinxSerialization)
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
@@ -111,7 +111,8 @@ android {
|
||||
create("general") {
|
||||
dimension = Constants.TYPE
|
||||
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
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(project(":logcatter"))
|
||||
|
||||
implementation(libs.androidx.core.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.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
@@ -158,14 +156,13 @@ dependencies {
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.manifest)
|
||||
|
||||
// get tunnel lib from github packages or mavenLocal
|
||||
// wg
|
||||
implementation(libs.tunnel)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
||||
// logging
|
||||
implementation(libs.timber)
|
||||
|
||||
|
||||
// compose navigation
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
@@ -175,6 +172,7 @@ dependencies {
|
||||
ksp(libs.hilt.android.compiler)
|
||||
|
||||
// accompanist
|
||||
implementation(libs.accompanist.systemuicontroller)
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
implementation(libs.accompanist.drawablepainter)
|
||||
@@ -195,13 +193,17 @@ dependencies {
|
||||
// serialization
|
||||
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
|
||||
implementation(libs.zxing.android.embedded)
|
||||
implementation(libs.zxing.core)
|
||||
|
||||
// bio
|
||||
implementation(libs.androidx.biometric.ktx)
|
||||
implementation(libs.pin.lock.compose)
|
||||
|
||||
// shortcuts
|
||||
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.platform.app.InstrumentationRegistry
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.Queries
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -23,13 +22,39 @@ class MigrationTest {
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrate6To7() {
|
||||
helper.createDatabase(dbName, 6).apply {
|
||||
fun migrate4To5() {
|
||||
helper.createDatabase(dbName, 4).apply {
|
||||
// Database has schema version 1. Insert some data using SQL queries.
|
||||
// You can't use DAO classes because they expect the latest schema.
|
||||
execSQL(Queries.createDefaultSettings())
|
||||
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.
|
||||
close()
|
||||
@@ -37,7 +62,7 @@ class MigrationTest {
|
||||
|
||||
// Re-open the database with version 2 and provide
|
||||
// MIGRATION_1_2 as the migration process.
|
||||
helper.runMigrationsAndValidate(dbName, 7, true)
|
||||
helper.runMigrationsAndValidate(dbName, 5, true)
|
||||
// MigrationTestHelper automatically verifies the schema changes,
|
||||
// but you need to validate that the data was migrated properly.
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<!--foreground service exempt android 14-->
|
||||
<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.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
|
||||
<!--foreground service permissions-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
@@ -52,7 +53,7 @@
|
||||
<application
|
||||
android:name=".WireGuardAutoTunnel"
|
||||
android:allowBackup="true"
|
||||
android:banner="@drawable/ic_banner"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
@@ -83,8 +84,7 @@
|
||||
android:screenOrientation="fullSensor"
|
||||
android:stateNotNeeded="true"
|
||||
android:theme="@style/zxing_CaptureTheme"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
tools:ignore="DiscouragedApi" />
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
<activity
|
||||
android:name=".service.shortcut.ShortcutsActivity"
|
||||
android:enabled="true"
|
||||
@@ -96,30 +96,13 @@
|
||||
android:name=".service.foreground.ForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:foregroundServiceType="systemExempted|specialUse"
|
||||
tools:node="merge" />
|
||||
<service
|
||||
android:name=".service.tile.TunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="Tunnel control"
|
||||
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:icon="@drawable/shield"
|
||||
android:label="WG Tunnel"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||
@@ -136,7 +119,7 @@
|
||||
android:name=".service.foreground.WireGuardTunnelService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:foregroundServiceType="systemExempted|specialUse"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:persistent="true"
|
||||
tools:node="merge">
|
||||
@@ -151,7 +134,7 @@
|
||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:foregroundServiceType="systemExempted|specialUse"
|
||||
android:persistent="true"
|
||||
android:stopWithTask="false"
|
||||
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.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
@HiltAndroidApp
|
||||
class WireGuardAutoTunnel : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
||||
PinManager.initialize(this)
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -29,18 +24,11 @@ class WireGuardAutoTunnel : Application() {
|
||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
}
|
||||
|
||||
fun requestTunnelTileServiceStateUpdate(context: Context) {
|
||||
fun requestTileServiceStateUpdate() {
|
||||
TileService.requestListeningState(
|
||||
context,
|
||||
instance,
|
||||
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.Database
|
||||
import androidx.room.DeleteColumn
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
|
||||
@Database(
|
||||
entities = [Settings::class, TunnelConfig::class],
|
||||
version = 7,
|
||||
version = 5,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
AutoMigration(from = 2, to = 3),
|
||||
AutoMigration(
|
||||
from = 3,
|
||||
to = 4,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 4,
|
||||
to = 5,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 5,
|
||||
to = 6,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 6,
|
||||
to = 7,
|
||||
spec = RemoveLegacySettingColumnsMigration::class,
|
||||
),
|
||||
],
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
AutoMigration(from = 2, to = 3),
|
||||
AutoMigration(
|
||||
from = 3,
|
||||
to = 4,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 4,
|
||||
to = 5,
|
||||
),
|
||||
],
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(DatabaseListConverters::class)
|
||||
@@ -42,13 +31,3 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
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
|
||||
fun stringToList(value: String): MutableList<String> {
|
||||
if (value.isBlank() || value.isEmpty()) return mutableListOf()
|
||||
if (value.isEmpty()) return mutableListOf()
|
||||
return try {
|
||||
Json.decodeFromString<MutableList<String>>(value)
|
||||
} 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
|
||||
interface SettingsDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun save(t: Settings)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: Settings)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun saveAll(t: List<Settings>)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<Settings>)
|
||||
|
||||
@Query("SELECT * FROM settings WHERE id=:id")
|
||||
suspend fun getById(id: Long): Settings?
|
||||
@Query("SELECT * FROM settings WHERE id=:id") suspend fun getById(id: Long): Settings?
|
||||
|
||||
@Query("SELECT * FROM settings")
|
||||
suspend fun getAll(): List<Settings>
|
||||
@Query("SELECT * FROM settings") suspend fun getAll(): List<Settings>
|
||||
|
||||
@Query("SELECT * FROM settings LIMIT 1")
|
||||
fun getSettingsFlow(): Flow<Settings>
|
||||
@Query("SELECT * FROM settings LIMIT 1") fun getSettingsFlow(): Flow<Settings>
|
||||
|
||||
@Query("SELECT * FROM settings")
|
||||
fun getAllFlow(): Flow<MutableList<Settings>>
|
||||
@Query("SELECT * FROM settings") fun getAllFlow(): Flow<MutableList<Settings>>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(t: Settings)
|
||||
@Delete suspend fun delete(t: Settings)
|
||||
|
||||
@Query("SELECT COUNT('id') FROM settings")
|
||||
suspend fun count(): Long
|
||||
@Query("SELECT COUNT('id') FROM settings") suspend fun count(): Long
|
||||
}
|
||||
|
||||
@@ -6,44 +6,21 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TunnelConfigDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun save(t: TunnelConfig)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(t: TunnelConfig)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun saveAll(t: TunnelConfigs)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id")
|
||||
suspend fun getById(id: Long): TunnelConfig?
|
||||
@Query("SELECT * FROM TunnelConfig WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig")
|
||||
suspend fun getAll(): TunnelConfigs
|
||||
@Query("SELECT * FROM TunnelConfig") suspend fun getAll(): List<TunnelConfig>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(t: TunnelConfig)
|
||||
@Delete suspend fun delete(t: TunnelConfig)
|
||||
|
||||
@Query("SELECT COUNT('id') FROM TunnelConfig")
|
||||
suspend fun count(): Long
|
||||
@Query("SELECT COUNT('id') FROM TunnelConfig") suspend fun count(): Long
|
||||
|
||||
@Query("SELECT * FROM TunnelConfig WHERE tunnel_networks LIKE '%' || :name || '%'")
|
||||
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>>
|
||||
@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.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
class DataStoreManager(private val context: Context) {
|
||||
companion object {
|
||||
val LOCATION_DISCLOSURE_SHOWN = booleanPreferencesKey("LOCATION_DISCLOSURE_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
|
||||
private val preferencesKey = "preferences"
|
||||
private val Context.dataStore by
|
||||
preferencesDataStore(
|
||||
name = preferencesKey,
|
||||
)
|
||||
preferencesDataStore(
|
||||
name = preferencesKey,
|
||||
)
|
||||
|
||||
suspend fun init() {
|
||||
try {
|
||||
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)
|
||||
}
|
||||
context.dataStore.data.first()
|
||||
}
|
||||
|
||||
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] }
|
||||
|
||||
suspend fun <T> getFromStore(key: Preferences.Key<T>): T? {
|
||||
return try {
|
||||
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()
|
||||
}
|
||||
suspend fun <T> getFromStore(key: Preferences.Key<T>) =
|
||||
context.dataStore.data.first { it.contains(key) }[key]
|
||||
|
||||
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
|
||||
data class Settings(
|
||||
@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")
|
||||
val isTunnelOnMobileDataEnabled: Boolean = false,
|
||||
var isTunnelOnMobileDataEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "trusted_network_ssids")
|
||||
val trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||
@ColumnInfo(name = "is_always_on_vpn_enabled") val isAlwaysOnVpnEnabled: Boolean = false,
|
||||
var trustedNetworkSSIDs: MutableList<String> = mutableListOf(),
|
||||
@ColumnInfo(name = "default_tunnel") var defaultTunnel: String? = null,
|
||||
@ColumnInfo(name = "is_always_on_vpn_enabled") var isAlwaysOnVpnEnabled: Boolean = false,
|
||||
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled")
|
||||
val isTunnelOnEthernetEnabled: Boolean = false,
|
||||
var isTunnelOnEthernetEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_shortcuts_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isShortcutsEnabled: Boolean = false,
|
||||
var isShortcutsEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_battery_saver_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isBatterySaverEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_tunnel_on_wifi_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isTunnelOnWifiEnabled: Boolean = false,
|
||||
var isTunnelOnWifiEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_kernel_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isKernelEnabled: Boolean = false,
|
||||
var isKernelEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_restore_on_boot_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isRestoreOnBootEnabled: Boolean = false,
|
||||
var isRestoreOnBootEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_multi_tunnel_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isMultiTunnelEnabled: Boolean = false,
|
||||
var isMultiTunnelEnabled: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_auto_tunnel_paused",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isAutoTunnelPaused: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_ping_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
val isPingEnabled: Boolean = false,
|
||||
)
|
||||
var isAutoTunnelPaused: Boolean = false,
|
||||
) {
|
||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
||||
return if (defaultTunnel != null) {
|
||||
val defaultConfig = TunnelConfig.from(defaultTunnel!!)
|
||||
(tunnelConfig.id == defaultConfig.id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,30 +5,26 @@ import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.wireguard.config.Config
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.InputStream
|
||||
|
||||
@Entity(indices = [Index(value = ["name"], unique = true)])
|
||||
@Serializable
|
||||
data class TunnelConfig(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "wg_quick") val 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,
|
||||
@ColumnInfo(name = "name") var name: String,
|
||||
@ColumnInfo(name = "wg_quick") var wgQuick: String
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return Json.encodeToString(serializer(), this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(string: String): TunnelConfig {
|
||||
return Json.decodeFromString<TunnelConfig>(string)
|
||||
}
|
||||
|
||||
fun configFromQuick(wgQuick: String): Config {
|
||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||
val reader = inputStream.bufferedReader(Charsets.UTF_8)
|
||||
|
||||
-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 kotlinx.coroutines.flow.Flow
|
||||
|
||||
class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository {
|
||||
class SettingsRepositoryImpl(private val settingsDoa: SettingsDao) : SettingsRepository {
|
||||
|
||||
override suspend fun save(settings: Settings) {
|
||||
settingsDoa.save(settings)
|
||||
-12
@@ -12,19 +12,7 @@ interface TunnelConfigRepository {
|
||||
|
||||
suspend fun save(tunnelConfig: TunnelConfig)
|
||||
|
||||
suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?)
|
||||
|
||||
suspend fun updateMobileDataTunnel(tunnelConfig: TunnelConfig?)
|
||||
|
||||
suspend fun delete(tunnelConfig: TunnelConfig)
|
||||
|
||||
suspend fun getById(id: Int): TunnelConfig?
|
||||
|
||||
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 com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -19,12 +18,11 @@ class DatabaseModule {
|
||||
@Singleton
|
||||
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
context.getString(R.string.db_name),
|
||||
)
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
context.getString(R.string.db_name),
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.addCallback(DatabaseCallback())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,4 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Kernel
|
||||
@Qualifier @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.TunnelConfigDao
|
||||
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.SettingsRepositoryImpl
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepositoryImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -38,13 +34,13 @@ class RepositoryModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideTunnelConfigRepository(tunnelConfigDao: TunnelConfigDao): TunnelConfigRepository {
|
||||
return RoomTunnelConfigRepository(tunnelConfigDao)
|
||||
return TunnelConfigRepositoryImpl(tunnelConfigDao)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSettingsRepository(settingsDao: SettingsDao): SettingsRepository {
|
||||
return RoomSettingsRepository(settingsDao)
|
||||
return SettingsRepositoryImpl(settingsDao)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@@ -52,22 +48,4 @@ class RepositoryModule {
|
||||
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStoreManager {
|
||||
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.util.RootShell
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
|
||||
import dagger.Module
|
||||
@@ -45,14 +44,8 @@ class TunnelModule {
|
||||
fun provideVpnService(
|
||||
@Userspace userspaceBackend: Backend,
|
||||
@Kernel kernelBackend: Backend,
|
||||
appDataRepository: AppDataRepository
|
||||
settingsRepository: SettingsRepository
|
||||
): VpnService {
|
||||
return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideServiceManager(appDataRepository: AppDataRepository): ServiceManager {
|
||||
return ServiceManager(appDataRepository)
|
||||
return WireGuardTunnel(userspaceBackend, kernelBackend, settingsRepository)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,4 @@ package com.zaneschepke.wireguardautotunnel.module
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class Userspace
|
||||
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace
|
||||
|
||||
@@ -3,43 +3,21 @@ package com.zaneschepke.wireguardautotunnel.receiver
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
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.util.goAsync
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||
context?.run {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
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)
|
||||
}
|
||||
if (settingsRepository.getSettings().isAutoTunnelEnabled) {
|
||||
ServiceManager.startWatcherServiceForeground(context!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-12
@@ -10,25 +10,20 @@ import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NotificationActionReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent?) = goAsync {
|
||||
try {
|
||||
//TODO fix for manual start changes when enabled
|
||||
serviceManager.stopVpnService(context)
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
serviceManager.startVpnServiceForeground(context)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.defaultTunnel != null) {
|
||||
ServiceManager.stopVpnService(context)
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
ServiceManager.startVpnServiceForeground(context, settings.defaultTunnel.toString())
|
||||
}
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
|
||||
+5
-8
@@ -4,7 +4,6 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import timber.log.Timber
|
||||
|
||||
open class ForegroundService : LifecycleService() {
|
||||
@@ -24,12 +23,11 @@ open class ForegroundService : LifecycleService() {
|
||||
when (action) {
|
||||
Action.START.name,
|
||||
Action.START_FOREGROUND.name -> startService(intent.extras)
|
||||
|
||||
Constants.ALWAYS_ON_VPN_ACTION -> {
|
||||
Timber.i("Always-on VPN starting service")
|
||||
Action.STOP.name -> stopService(intent.extras)
|
||||
"android.net.VpnService" -> {
|
||||
Timber.d("Always-on VPN starting service")
|
||||
startService(intent.extras)
|
||||
}
|
||||
|
||||
else -> Timber.d("This should never happen. No action in the received intent")
|
||||
}
|
||||
} else {
|
||||
@@ -44,7 +42,6 @@ open class ForegroundService : LifecycleService() {
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Timber.d("The service has been destroyed")
|
||||
stopService()
|
||||
}
|
||||
|
||||
protected open fun startService(extras: Bundle?) {
|
||||
@@ -53,13 +50,13 @@ open class ForegroundService : LifecycleService() {
|
||||
isServiceStarted = true
|
||||
}
|
||||
|
||||
protected open fun stopService() {
|
||||
protected open fun stopService(extras: Bundle?) {
|
||||
Timber.d("Stopping ${this.javaClass.simpleName}")
|
||||
try {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Timber.d("Service stopped without being started: ${e.message}")
|
||||
}
|
||||
isServiceStarted = false
|
||||
}
|
||||
|
||||
+31
-34
@@ -3,17 +3,31 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
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(
|
||||
action: Action,
|
||||
context: Context,
|
||||
cls: Class<T>,
|
||||
extras: Map<String, Int>? = null
|
||||
extras: Map<String, String>? = null
|
||||
) {
|
||||
val intent =
|
||||
Intent(context, cls).also {
|
||||
@@ -23,32 +37,30 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
intent.component?.javaClass
|
||||
try {
|
||||
when (action) {
|
||||
Action.START_FOREGROUND -> context.startForegroundService(intent)
|
||||
Action.START -> context.startService(intent)
|
||||
Action.STOP -> context.stopService(intent)
|
||||
Action.START_FOREGROUND -> {
|
||||
context.startForegroundService(intent)
|
||||
}
|
||||
Action.START -> {
|
||||
context.startService(intent)
|
||||
}
|
||||
Action.STOP -> context.startService(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startVpnService(
|
||||
context: Context,
|
||||
tunnelId: Int? = null,
|
||||
isManualStart: Boolean = false
|
||||
) {
|
||||
if (isManualStart) onManualStart(tunnelId)
|
||||
fun startVpnService(context: Context, tunnelConfig: String) {
|
||||
actionOnService(
|
||||
Action.START,
|
||||
context,
|
||||
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) {
|
||||
if (isManualStop) onManualStop()
|
||||
Timber.i("Stopping vpn service")
|
||||
fun stopVpnService(context: Context) {
|
||||
Timber.d("Stopping vpn service action")
|
||||
actionOnService(
|
||||
Action.STOP,
|
||||
context,
|
||||
@@ -56,27 +68,12 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun onManualStop() {
|
||||
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)
|
||||
fun startVpnServiceForeground(context: Context, tunnelConfig: String) {
|
||||
actionOnService(
|
||||
Action.START_FOREGROUND,
|
||||
context,
|
||||
WireGuardTunnelService::class.java,
|
||||
tunnelId?.let { mapOf(Constants.TUNNEL_EXTRA_KEY to it) },
|
||||
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
enum class ServiceState {
|
||||
STARTED,
|
||||
STOPPED,
|
||||
}
|
||||
-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()))
|
||||
}
|
||||
}
|
||||
|
||||
+142
-178
@@ -1,14 +1,18 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.os.SystemClock
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
|
||||
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
|
||||
@@ -24,38 +28,37 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.net.InetAddress
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
private val foregroundId = 122
|
||||
|
||||
@Inject
|
||||
lateinit var wifiService: NetworkService<WifiService>
|
||||
@Inject lateinit var wifiService: NetworkService<WifiService>
|
||||
|
||||
@Inject
|
||||
lateinit var mobileDataService: NetworkService<MobileDataService>
|
||||
@Inject lateinit var mobileDataService: NetworkService<MobileDataService>
|
||||
|
||||
@Inject
|
||||
lateinit var ethernetService: NetworkService<EthernetService>
|
||||
@Inject lateinit var ethernetService: NetworkService<EthernetService>
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var notificationService: NotificationService
|
||||
@Inject lateinit var notificationService: NotificationService
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService: VpnService
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
@Inject lateinit var vpnService: VpnService
|
||||
|
||||
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 var wakeLock: PowerManager.WakeLock? = null
|
||||
@@ -65,7 +68,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
if (appDataRepository.settings.getSettings().isAutoTunnelPaused) {
|
||||
if (settingsRepository.getSettings().isAutoTunnelPaused) {
|
||||
launchWatcherPausedNotification()
|
||||
} else launchWatcherNotification()
|
||||
} catch (e: Exception) {
|
||||
@@ -74,11 +77,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
stopService()
|
||||
}
|
||||
|
||||
override fun startService(extras: Bundle?) {
|
||||
super.startService(extras)
|
||||
try {
|
||||
@@ -91,8 +89,8 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopService() {
|
||||
super.stopService()
|
||||
override fun stopService(extras: Bundle?) {
|
||||
super.stopService(extras)
|
||||
wakeLock?.let {
|
||||
if (it.isHeld) {
|
||||
it.release()
|
||||
@@ -124,15 +122,42 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
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 =
|
||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||
try {
|
||||
Timber.i("Initiating wakelock with 10 min timeout")
|
||||
if (isBatterySaverOn) {
|
||||
Timber.d("Initiating wakelock with timeout")
|
||||
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||
} finally {
|
||||
release()
|
||||
} else {
|
||||
Timber.d("Initiating wakelock with zero timeout")
|
||||
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,42 +172,35 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
private fun startWatcherJob() {
|
||||
watcherJob =
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val setting = appDataRepository.settings.getSettings()
|
||||
val setting = settingsRepository.getSettings()
|
||||
launch {
|
||||
Timber.i("Starting wifi watcher")
|
||||
Timber.d("Starting wifi watcher")
|
||||
watchForWifiConnectivityChanges()
|
||||
}
|
||||
if (setting.isTunnelOnMobileDataEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting mobile data watcher")
|
||||
Timber.d("Starting mobile data watcher")
|
||||
watchForMobileDataConnectivityChanges()
|
||||
}
|
||||
}
|
||||
if (setting.isTunnelOnEthernetEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting ethernet data watcher")
|
||||
Timber.d("Starting ethernet data watcher")
|
||||
watchForEthernetConnectivityChanges()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting vpn state watcher")
|
||||
Timber.d("Starting vpn state watcher")
|
||||
watchForVpnConnectivityChanges()
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting settings watcher")
|
||||
Timber.d("Starting settings watcher")
|
||||
watchForSettingsChanges()
|
||||
}
|
||||
if (setting.isPingEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting ping watcher")
|
||||
watchForPingFailure()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.i("Starting management watcher")
|
||||
Timber.d("Starting management watcher")
|
||||
manageVpn()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,68 +208,32 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
mobileDataService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Mobile data connection")
|
||||
Timber.d("Gained Mobile data connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
Timber.i("Mobile data capabilities changed")
|
||||
Timber.d("Mobile data capabilities changed")
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
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() {
|
||||
appDataRepository.settings.getSettingsFlow().collect {
|
||||
settingsRepository.getSettingsFlow().collect {
|
||||
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||
when (it.isAutoTunnelPaused) {
|
||||
true -> launchWatcherPausedNotification()
|
||||
@@ -267,11 +249,19 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
|
||||
private suspend fun watchForVpnConnectivityChanges() {
|
||||
vpnService.vpnState.collect {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
vpnStatus = it.status,
|
||||
config = it.tunnelConfig,
|
||||
)
|
||||
when (it.status) {
|
||||
Tunnel.State.DOWN ->
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isVpnConnected = false,
|
||||
)
|
||||
Tunnel.State.UP ->
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isVpnConnected = true,
|
||||
)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,27 +269,25 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
ethernetService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Ethernet connection")
|
||||
Timber.d("Gained Ethernet connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Ethernet capabilities changed")
|
||||
Timber.d("Ethernet capabilities changed")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = false,
|
||||
)
|
||||
Timber.i("Lost Ethernet connection")
|
||||
Timber.d("Lost Ethernet connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,119 +297,95 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
wifiService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.i("Gained Wi-Fi connection")
|
||||
Timber.d("Gained Wi-Fi connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.i("Wifi capabilities changed")
|
||||
Timber.d("Wifi capabilities changed")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
val ssid = wifiService.getNetworkName(it.networkCapabilities)
|
||||
ssid?.let {
|
||||
Timber.i("Detected SSID: $ssid")
|
||||
appDataRepository.appState.setCurrentSsid(ssid)
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
currentNetworkSSID = ssid,
|
||||
)
|
||||
} ?: Timber.w("Failed to read ssid")
|
||||
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||
Timber.d("Detected SSID: $ssid")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
currentNetworkSSID = ssid,
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = false,
|
||||
)
|
||||
Timber.i("Lost Wi-Fi connection")
|
||||
Timber.d("Lost Wi-Fi connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getMobileDataTunnel(): TunnelConfig? {
|
||||
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
|
||||
}
|
||||
|
||||
private suspend fun getSsidTunnel(ssid: String): TunnelConfig? {
|
||||
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
|
||||
}
|
||||
|
||||
// TODO clean this up
|
||||
private suspend fun manageVpn() {
|
||||
networkEventsFlow.collectLatest { watcherState ->
|
||||
val autoTunnel = "Auto-tunnel watcher"
|
||||
if (!watcherState.settings.isAutoTunnelPaused) {
|
||||
//delay for rapid network state changes and then collect latest
|
||||
delay(Constants.WATCHER_COLLECTION_DELAY)
|
||||
networkEventsFlow.collectLatest {
|
||||
Timber.i("New watcher state: $it")
|
||||
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
when {
|
||||
watcherState.isEthernetConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
|
||||
serviceManager.startVpnServiceForeground(this)
|
||||
((it.isEthernetConnected &&
|
||||
it.settings.isTunnelOnEthernetEnabled &&
|
||||
!it.isVpnConnected)) -> {
|
||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||
Timber.i("Condition 1 met")
|
||||
}
|
||||
|
||||
watcherState.isMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on on mobile data condition met")
|
||||
serviceManager.startVpnServiceForeground(this, getMobileDataTunnel()?.id)
|
||||
(!it.isEthernetConnected &&
|
||||
it.settings.isTunnelOnMobileDataEnabled &&
|
||||
!it.isWifiConnected &&
|
||||
it.isMobileDataConnected &&
|
||||
!it.isVpnConnected) -> {
|
||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||
Timber.i("Condition 2 met")
|
||||
}
|
||||
|
||||
watcherState.isTunnelNotMobileDataPreferredConditionMet() -> {
|
||||
getMobileDataTunnel()?.let {
|
||||
Timber.i("$autoTunnel - tunnel connected on mobile data is not preferred condition met, switching to preferred")
|
||||
serviceManager.startVpnServiceForeground(
|
||||
this,
|
||||
getMobileDataTunnel()?.id,
|
||||
)
|
||||
}
|
||||
(!it.isEthernetConnected &&
|
||||
!it.settings.isTunnelOnMobileDataEnabled &&
|
||||
!it.isWifiConnected &&
|
||||
it.isVpnConnected) -> {
|
||||
ServiceManager.stopVpnService(this)
|
||||
Timber.i("Condition 3 met")
|
||||
}
|
||||
|
||||
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
|
||||
serviceManager.stopVpnService(this)
|
||||
(!it.isEthernetConnected &&
|
||||
it.isWifiConnected &&
|
||||
!it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID) &&
|
||||
it.settings.isTunnelOnWifiEnabled &&
|
||||
(!it.isVpnConnected)) -> {
|
||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||
Timber.i("Condition 4 met")
|
||||
}
|
||||
|
||||
watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> {
|
||||
Timber.i("$autoTunnel - tunnel on ssid not associated with current tunnel condition met")
|
||||
getSsidTunnel(watcherState.currentNetworkSSID)?.let {
|
||||
Timber.i("Found tunnel associated with this SSID, bringing tunnel up")
|
||||
serviceManager.startVpnServiceForeground(this, it.id)
|
||||
} ?: suspend {
|
||||
Timber.i("No tunnel associated with this SSID, using defaults")
|
||||
if (appDataRepository.getPrimaryOrFirstTunnel()?.name != vpnService.name) {
|
||||
serviceManager.startVpnServiceForeground(this)
|
||||
}
|
||||
}.invoke()
|
||||
(!it.isEthernetConnected &&
|
||||
(it.isWifiConnected &&
|
||||
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
||||
(it.isVpnConnected)) -> {
|
||||
ServiceManager.stopVpnService(this)
|
||||
Timber.i("Condition 5 met")
|
||||
}
|
||||
|
||||
watcherState.isUntrustedWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel on untrusted wifi condition met")
|
||||
serviceManager.startVpnServiceForeground(
|
||||
this,
|
||||
getSsidTunnel(watcherState.currentNetworkSSID)?.id,
|
||||
)
|
||||
(!it.isEthernetConnected &&
|
||||
(it.isWifiConnected &&
|
||||
!it.settings.isTunnelOnWifiEnabled &&
|
||||
(it.isVpnConnected))) -> {
|
||||
ServiceManager.stopVpnService(this)
|
||||
Timber.i("Condition 6 met")
|
||||
}
|
||||
|
||||
watcherState.isTrustedWifiConditionMet() -> {
|
||||
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
|
||||
serviceManager.stopVpnService(this)
|
||||
(!it.isEthernetConnected &&
|
||||
!it.isWifiConnected &&
|
||||
!it.isMobileDataConnected &&
|
||||
(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 -> {
|
||||
Timber.i("$autoTunnel - no condition met")
|
||||
Timber.i("No condition met")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+72
-78
@@ -5,9 +5,10 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
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.service.notification.NotificationService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
@@ -20,110 +21,103 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WireGuardTunnelService : ForegroundService() {
|
||||
private val foregroundId = 123
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService: VpnService
|
||||
@Inject lateinit var vpnService: VpnService
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var notificationService: NotificationService
|
||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
|
||||
@Inject lateinit var notificationService: NotificationService
|
||||
|
||||
private lateinit var job: Job
|
||||
|
||||
private var tunnelName: String = ""
|
||||
private var didShowConnected = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
//TODO fix this to not launch if AOVPN
|
||||
if (appDataRepository.tunnels.count() != 0) {
|
||||
if (tunnelConfigRepository.getAll().isNotEmpty()) {
|
||||
launchVpnNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
}
|
||||
|
||||
override fun startService(extras: Bundle?) {
|
||||
super.startService(extras)
|
||||
cancelJob()
|
||||
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
|
||||
val tunnelConfig = tunnelConfigString?.let { TunnelConfig.from(it) }
|
||||
tunnelName = tunnelConfig?.name ?: ""
|
||||
job =
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
launch {
|
||||
val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY)
|
||||
if (vpnService.getState() == Tunnel.State.UP) {
|
||||
vpnService.stopTunnel()
|
||||
}
|
||||
vpnService.startTunnel(
|
||||
tunnelId?.let {
|
||||
appDataRepository.tunnels.getById(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
launch {
|
||||
handshakeNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO improve tunnel notifications
|
||||
private suspend fun handshakeNotifications() {
|
||||
var tunnelName: String? = null
|
||||
vpnService.vpnState.collect { state ->
|
||||
state.statistics
|
||||
?.mapPeerStats()
|
||||
?.map { it.value?.handshakeStatus() }
|
||||
.let { statuses ->
|
||||
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
|
||||
if (tunnelConfig != null) {
|
||||
try {
|
||||
tunnelName = tunnelConfig.name
|
||||
vpnService.startTunnel(tunnelConfig)
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Problem starting tunnel: ${e.message}")
|
||||
stopService(extras)
|
||||
}
|
||||
} else {
|
||||
Timber.d("Tunnel config null, starting default tunnel or first")
|
||||
val settings = settingsRepository.getSettings()
|
||||
val tunnels = tunnelConfigRepository.getAll()
|
||||
if (settings.isAlwaysOnVpnEnabled) {
|
||||
val tunnel =
|
||||
if (settings.defaultTunnel != null) {
|
||||
TunnelConfig.from(settings.defaultTunnel!!)
|
||||
} else if (tunnels.isNotEmpty()) {
|
||||
tunnels.first()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (tunnel != null) {
|
||||
tunnelName = tunnel.name
|
||||
vpnService.startTunnel(tunnel)
|
||||
}
|
||||
}
|
||||
|
||||
statuses?.any { it == HandshakeStatus.STALE } == true -> {}
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } ==
|
||||
true -> {
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
if (state.status == Tunnel.State.UP && state.tunnelConfig?.name != tunnelName) {
|
||||
tunnelName = state.tunnelConfig?.name
|
||||
launchVpnNotification(
|
||||
getString(R.string.tunnel_start_title),
|
||||
"${getString(R.string.tunnel_start_text)} - $tunnelName",
|
||||
)
|
||||
// TODO add failed to connect notification
|
||||
launch {
|
||||
vpnService.vpnState.collect { state ->
|
||||
state.statistics
|
||||
?.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() {
|
||||
super.stopService()
|
||||
override fun stopService(extras: Bundle?) {
|
||||
super.stopService(extras)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
vpnService.stopTunnel()
|
||||
didShowConnected = false
|
||||
@@ -160,12 +154,12 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
channelId = getString(R.string.vpn_channel_id),
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
action =
|
||||
PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(this, NotificationActionReceiver::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
),
|
||||
PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(this, NotificationActionReceiver::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
),
|
||||
actionText = getString(R.string.restart),
|
||||
title = getString(R.string.vpn_connection_failed),
|
||||
onGoing = false,
|
||||
|
||||
+1
-2
@@ -53,7 +53,6 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
@@ -118,7 +117,7 @@ inline fun <Result> Flow<NetworkStatus>.map(
|
||||
crossinline onUnavailable: suspend (network: Network) -> Result,
|
||||
crossinline onAvailable: suspend (network: Network) -> Result,
|
||||
crossinline onCapabilitiesChanged:
|
||||
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
|
||||
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result
|
||||
): Flow<Result> = map { status ->
|
||||
when (status) {
|
||||
is NetworkStatus.Unavailable -> onUnavailable(status.network)
|
||||
|
||||
+5
-5
@@ -45,10 +45,10 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
|
||||
): Notification {
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
channelId,
|
||||
channelName,
|
||||
importance,
|
||||
)
|
||||
channelId,
|
||||
channelName,
|
||||
importance,
|
||||
)
|
||||
.let {
|
||||
it.description = title
|
||||
it.enableLights(lights)
|
||||
@@ -94,7 +94,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
|
||||
.setOngoing(onGoing)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setShowWhen(showTimestamp)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setSmallIcon(R.mipmap.ic_launcher_foreground)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
+53
-39
@@ -1,66 +1,80 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.shortcut
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.ComponentActivity
|
||||
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.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShortcutsActivity : ComponentActivity() {
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
private suspend fun toggleWatcherServicePause() {
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
val pauseAutoTunnel = !settings.isAutoTunnelPaused
|
||||
settingsRepository.save(
|
||||
settings.copy(
|
||||
isAutoTunnelPaused = pauseAutoTunnel,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
if (settings.isShortcutsEnabled) {
|
||||
when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) {
|
||||
WireGuardTunnelService::class.java.simpleName -> {
|
||||
setContentView(View(this))
|
||||
if (
|
||||
intent
|
||||
.getStringExtra(CLASS_NAME_EXTRA_KEY)
|
||||
.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 tunnelConfig = tunnelName?.let {
|
||||
appDataRepository.tunnels.getAll().firstOrNull {
|
||||
it.name == tunnelName
|
||||
val tunnelConfig =
|
||||
if (tunnelName != null) {
|
||||
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) {
|
||||
Action.START.name -> serviceManager.startVpnServiceForeground(
|
||||
this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true,
|
||||
)
|
||||
|
||||
Action.STOP.name -> serviceManager.stopVpnService(
|
||||
this@ShortcutsActivity,
|
||||
isManualStop = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
)
|
||||
Action.STOP.name ->
|
||||
ServiceManager.stopVpnService(
|
||||
this@ShortcutsActivity,
|
||||
)
|
||||
Action.START.name ->
|
||||
ServiceManager.startVpnServiceForeground(
|
||||
this@ShortcutsActivity,
|
||||
tunnelConfig.toString(),
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.message)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-111
@@ -1,111 +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 onTileAdded() {
|
||||
super.onTileAdded()
|
||||
onStartListening()
|
||||
}
|
||||
|
||||
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
-37
@@ -5,7 +5,8 @@ import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
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.tunnel.VpnService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -17,44 +18,41 @@ import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TunnelControlTile : TileService() {
|
||||
class TunnelControlTile() : TileService() {
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
|
||||
@Inject
|
||||
lateinit var vpnService: VpnService
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
@Inject lateinit var vpnService: VpnService
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private var manualStartConfig: TunnelConfig? = null
|
||||
private var tunnelName: String? = null
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
Timber.d("On start listening called")
|
||||
scope.launch {
|
||||
vpnService.vpnState.collect { it ->
|
||||
vpnService.vpnState.collect {
|
||||
when (it.status) {
|
||||
Tunnel.State.UP -> {
|
||||
setActive()
|
||||
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()
|
||||
}
|
||||
|
||||
Tunnel.State.UP -> setActive()
|
||||
Tunnel.State.DOWN -> 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 ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,24 +67,20 @@ class TunnelControlTile : TileService() {
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
onStartListening()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
scope.launch {
|
||||
try {
|
||||
val tunnelConfig =
|
||||
tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
||||
toggleWatcherServicePause()
|
||||
if (vpnService.getState() == Tunnel.State.UP) {
|
||||
serviceManager.stopVpnService(
|
||||
this@TunnelControlTile,
|
||||
isManualStop = true,
|
||||
)
|
||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||
} else {
|
||||
serviceManager.startVpnServiceForeground(
|
||||
this@TunnelControlTile, manualStartConfig?.id, isManualStart = true,
|
||||
ServiceManager.startVpnServiceForeground(
|
||||
this@TunnelControlTile,
|
||||
tunnelConfig.toString(),
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -98,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() {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
@@ -109,7 +117,6 @@ class TunnelControlTile : TileService() {
|
||||
}
|
||||
|
||||
private fun setUnavailable() {
|
||||
manualStartConfig = null
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface VpnService : Tunnel {
|
||||
suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State
|
||||
suspend fun startTunnel(tunnelConfig: TunnelConfig): Tunnel.State
|
||||
|
||||
suspend fun stopTunnel()
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
|
||||
data class VpnState(
|
||||
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||
val tunnelConfig: TunnelConfig? = null,
|
||||
val name: String = "",
|
||||
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.Statistics
|
||||
import com.wireguard.android.backend.Tunnel.State
|
||||
import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
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.Userspace
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
@@ -26,7 +27,7 @@ class WireGuardTunnel
|
||||
constructor(
|
||||
@Userspace private val userspaceBackend: Backend,
|
||||
@Kernel private val kernelBackend: Backend,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
private val settingsRepository: SettingsRepository
|
||||
) : VpnService {
|
||||
private val _vpnState = MutableStateFlow(VpnState())
|
||||
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow()
|
||||
@@ -35,13 +36,15 @@ constructor(
|
||||
|
||||
private lateinit var statsJob: Job
|
||||
|
||||
private var config: Config? = null
|
||||
|
||||
private var backend: Backend = userspaceBackend
|
||||
|
||||
private var backendIsUserspace = true
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
appDataRepository.settings.getSettingsFlow().collect {
|
||||
settingsRepository.getSettingsFlow().collect {
|
||||
if (it.isKernelEnabled && backendIsUserspace) {
|
||||
Timber.d("Setting kernel backend")
|
||||
backend = kernelBackend
|
||||
@@ -55,22 +58,20 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig?): State {
|
||||
override suspend fun startTunnel(tunnelConfig: TunnelConfig): State {
|
||||
return try {
|
||||
//TODO we need better error handling here
|
||||
val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()
|
||||
if (config != null) {
|
||||
emitTunnelConfig(config)
|
||||
val wgConfig = TunnelConfig.configFromQuick(config.wgQuick)
|
||||
val state =
|
||||
backend.setState(
|
||||
this,
|
||||
State.UP,
|
||||
wgConfig,
|
||||
)
|
||||
state
|
||||
} else throw Exception("No tunnels")
|
||||
} catch (e: BackendException) {
|
||||
stopTunnelOnConfigChange(tunnelConfig)
|
||||
emitTunnelName(tunnelConfig.name)
|
||||
config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||
val state =
|
||||
backend.setState(
|
||||
this,
|
||||
State.UP,
|
||||
config,
|
||||
)
|
||||
emitTunnelState(state)
|
||||
state
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||
State.DOWN
|
||||
}
|
||||
@@ -92,14 +93,24 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun emitTunnelConfig(tunnelConfig: TunnelConfig?) {
|
||||
private suspend fun emitTunnelName(name: String) {
|
||||
_vpnState.emit(
|
||||
_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() {
|
||||
try {
|
||||
if (getState() == State.UP) {
|
||||
@@ -115,14 +126,10 @@ constructor(
|
||||
return backend.getState(this)
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return _vpnState.value.tunnelConfig?.name ?: ""
|
||||
}
|
||||
|
||||
override fun onStateChange(state: State) {
|
||||
val tunnel = this
|
||||
emitTunnelState(state)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
if (state == State.UP) {
|
||||
statsJob =
|
||||
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
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.SystemBarStyle
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
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.Scaffold
|
||||
import androidx.compose.material3.SnackbarData
|
||||
@@ -22,43 +21,41 @@ import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
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.prompt.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||
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.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.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -67,98 +64,83 @@ class MainActivity : AppCompatActivity() {
|
||||
@Inject
|
||||
lateinit var dataStoreManager: DataStoreManager
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
@OptIn(
|
||||
ExperimentalPermissionsApi::class,
|
||||
)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
|
||||
|
||||
// load preferences into memory and init data
|
||||
lifecycleScope.launch {
|
||||
dataStoreManager.init()
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(this@MainActivity)
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
serviceManager.startWatcherService(application.applicationContext)
|
||||
try {
|
||||
dataStoreManager.init()
|
||||
if (settingsRepository.getAll().isEmpty()) {
|
||||
settingsRepository.save(com.zaneschepke.wireguardautotunnel.data.model.Settings())
|
||||
}
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
} catch (e: IOException) {
|
||||
Timber.e("Failed to load preferences")
|
||||
}
|
||||
}
|
||||
setContent {
|
||||
val appViewModel = hiltViewModel<AppViewModel>()
|
||||
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
|
||||
// val activityViewModel = hiltViewModel<ActivityViewModel>()
|
||||
|
||||
val navController = rememberNavController()
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
WireguardAutoTunnelTheme {
|
||||
LaunchedEffect(Unit) {
|
||||
appViewModel.setNotificationPermissionAccepted(
|
||||
notificationPermissionState?.status?.isGranted ?: true,
|
||||
)
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
|
||||
}
|
||||
TransparentSystemBars()
|
||||
|
||||
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
||||
if (!appUiState.snackbarMessageConsumed) {
|
||||
showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage))
|
||||
appViewModel.snackbarMessageConsumed()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val notificationPermissionState =
|
||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
||||
fun requestNotificationPermission() {
|
||||
if (
|
||||
!notificationPermissionState.status.isGranted &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
||||
) {
|
||||
notificationPermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
snackbarHost = {
|
||||
@@ -167,47 +149,65 @@ class MainActivity : AppCompatActivity() {
|
||||
snackbarData.visuals.message,
|
||||
isRtl = false,
|
||||
containerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
2.dp,
|
||||
),
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
2.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
//TODO refactor
|
||||
modifier = Modifier
|
||||
.focusable()
|
||||
.focusProperties {
|
||||
when (navBackStackEntry?.destination?.route) {
|
||||
Screen.Lock.route -> Unit
|
||||
else -> up = focusRequester
|
||||
modifier = Modifier.focusable().focusProperties { up = focusRequester },
|
||||
bottomBar =
|
||||
if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
||||
{
|
||||
BottomNavBar(
|
||||
navController,
|
||||
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 ->
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination =
|
||||
(if (PinManager.pinExists()) Screen.Lock.route else Screen.Main.route),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(padding)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
if (vpnIntent != null) {
|
||||
PermissionRequestFailedScreen(
|
||||
padding = padding,
|
||||
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
|
||||
message = getString(R.string.vpn_permission_required),
|
||||
getString(R.string.retry),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
if (!notificationPermissionState.status.isGranted) {
|
||||
PermissionRequestFailedScreen(
|
||||
padding = padding,
|
||||
onRequestAgain = {
|
||||
val intentSettings =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data =
|
||||
Uri.fromParts(
|
||||
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(
|
||||
Screen.Main.route,
|
||||
) {
|
||||
MainScreen(
|
||||
padding = padding,
|
||||
focusRequester = focusRequester,
|
||||
appViewModel = appViewModel,
|
||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
@@ -215,8 +215,8 @@ class MainActivity : AppCompatActivity() {
|
||||
Screen.Settings.route,
|
||||
) {
|
||||
SettingsScreen(
|
||||
appViewModel = appViewModel,
|
||||
navController = navController,
|
||||
padding = padding,
|
||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
}
|
||||
@@ -224,42 +224,25 @@ class MainActivity : AppCompatActivity() {
|
||||
Screen.Support.route,
|
||||
) {
|
||||
SupportScreen(
|
||||
padding = padding,
|
||||
focusRequester = focusRequester,
|
||||
appViewModel = appViewModel,
|
||||
navController = navController,
|
||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||
)
|
||||
}
|
||||
composable(Screen.Support.Logs.route) {
|
||||
LogsScreen(appViewModel)
|
||||
}
|
||||
composable("${Screen.Config.route}/{id}") {
|
||||
val id = it.arguments?.getString("id")
|
||||
if (!id.isNullOrBlank()) {
|
||||
// https://dagger.dev/hilt/view-model#assisted-injection
|
||||
ConfigScreen(
|
||||
navController = navController,
|
||||
tunnelId = id,
|
||||
appViewModel = appViewModel,
|
||||
id = id,
|
||||
showSnackbarMessage = { message ->
|
||||
showSnackBarMessage(message)
|
||||
},
|
||||
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,
|
||||
icon = Icons.Rounded.QuestionMark,
|
||||
)
|
||||
|
||||
data object Logs : Screen("support/logs")
|
||||
}
|
||||
|
||||
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.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
@Composable
|
||||
fun ClickableIconButton(
|
||||
@@ -27,12 +29,9 @@ fun ClickableIconButton(
|
||||
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = icon.name,
|
||||
contentDescription = stringResource(R.string.delete),
|
||||
modifier =
|
||||
Modifier
|
||||
.size(ButtonDefaults.IconSize)
|
||||
.weight(1f, false)
|
||||
.clickable {
|
||||
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
||||
if (enabled) {
|
||||
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(
|
||||
modifier =
|
||||
Modifier
|
||||
.animateContentSize()
|
||||
.clip(RoundedCornerShape(30.dp))
|
||||
.combinedClickable(
|
||||
onClick = { onClick() },
|
||||
onLongClick = { onHold() },
|
||||
),
|
||||
Modifier.animateContentSize()
|
||||
.clip(RoundedCornerShape(30.dp))
|
||||
.combinedClickable(
|
||||
onClick = { onClick() },
|
||||
onLongClick = { onHold() },
|
||||
),
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 15.dp, vertical = 5.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp, vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(.60f),
|
||||
) {
|
||||
icon()
|
||||
Text(text)
|
||||
@@ -62,13 +60,11 @@ fun RowListItem(
|
||||
statistics?.peers()?.forEach {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
//TODO change these to string resources
|
||||
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||
val peerTx = statistics.peer(it)!!.txBytes
|
||||
val peerRx = statistics.peer(it)!!.rxBytes
|
||||
|
||||
@@ -63,18 +63,17 @@ fun SearchBar(onQuery: (queryString: String) -> Unit) {
|
||||
},
|
||||
maxLines = 1,
|
||||
colors =
|
||||
TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
),
|
||||
TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
),
|
||||
placeholder = { Text(text = stringResource(R.string.hint_search_packages)) },
|
||||
textStyle = MaterialTheme.typography.bodySmall,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
|
||||
Modifier.fillMaxWidth()
|
||||
.background(color = MaterialTheme.colorScheme.background, shape = RectangleShape),
|
||||
)
|
||||
}
|
||||
|
||||
+4
-4
@@ -27,10 +27,10 @@ fun ConfigurationTextBox(
|
||||
maxLines = 1,
|
||||
placeholder = { Text(hint) },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
}
|
||||
|
||||
+2
-5
@@ -9,7 +9,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
@Composable
|
||||
@@ -22,13 +21,11 @@ fun ConfigurationToggle(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(padding),
|
||||
modifier = Modifier.fillMaxWidth().padding(padding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(label, textAlign = TextAlign.Start)
|
||||
Text(label)
|
||||
Switch(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
|
||||
+2
-17
@@ -6,33 +6,18 @@ import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
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.navigation.NavController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
|
||||
@Composable
|
||||
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
|
||||
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(
|
||||
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
|
||||
|
||||
NavigationBarItem(
|
||||
|
||||
-6
@@ -21,32 +21,26 @@ fun AuthorizationPrompt(onSuccess: () -> Unit, onFailure: () -> Unit, onError: (
|
||||
onError("Biometrics not available")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
||||
onError("Biometrics not created")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
||||
onError("Biometric hardware not found")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
||||
onError("Biometric security update required")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
||||
onError("Biometrics not supported")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
||||
onError("Biometrics status unknown")
|
||||
false
|
||||
}
|
||||
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
+11
-11
@@ -19,9 +19,12 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
|
||||
@Composable
|
||||
@@ -30,30 +33,27 @@ fun CustomSnackBar(
|
||||
isRtl: Boolean = true,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Snackbar(
|
||||
containerColor = containerColor,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
|
||||
)
|
||||
.padding(bottom = 100.dp),
|
||||
Modifier.fillMaxWidth(
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) 1 / 3f else 2 / 3f,
|
||||
)
|
||||
.padding(bottom = 100.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.width(IntrinsicSize.Max)
|
||||
.height(IntrinsicSize.Min),
|
||||
modifier = Modifier.width(IntrinsicSize.Max).height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
) {
|
||||
val icon = Icons.Rounded.Info
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = icon.name,
|
||||
Icons.Rounded.Info,
|
||||
contentDescription = stringResource(R.string.info),
|
||||
tint = Color.White,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
)
|
||||
|
||||
+1
-4
@@ -16,10 +16,7 @@ fun LoadingScreen() {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.focusable()
|
||||
.padding(),
|
||||
modifier = Modifier.fillMaxSize().focusable().padding(),
|
||||
) {
|
||||
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) {
|
||||
Text(
|
||||
title,
|
||||
textAlign = TextAlign.Start,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.ExtraBold),
|
||||
modifier = Modifier.padding(padding, bottom = 5.dp, top = 5.dp),
|
||||
)
|
||||
|
||||
@@ -18,11 +18,11 @@ data class InterfaceProxy(
|
||||
addresses = i.addresses.joinToString(", ").trim(),
|
||||
dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(),
|
||||
listenPort =
|
||||
if (i.listenPort.isPresent) {
|
||||
i.listenPort.get().toString().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if (i.listenPort.isPresent) {
|
||||
i.listenPort.get().toString().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,23 +14,23 @@ data class PeerProxy(
|
||||
return PeerProxy(
|
||||
publicKey = peer.publicKey.toBase64(),
|
||||
preSharedKey =
|
||||
if (peer.preSharedKey.isPresent) {
|
||||
peer.preSharedKey.get().toBase64().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if (peer.preSharedKey.isPresent) {
|
||||
peer.preSharedKey.get().toBase64().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
persistentKeepalive =
|
||||
if (peer.persistentKeepalive.isPresent) {
|
||||
peer.persistentKeepalive.get().toString().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if (peer.persistentKeepalive.isPresent) {
|
||||
peer.persistentKeepalive.get().toString().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
endpoint =
|
||||
if (peer.endpoint.isPresent) {
|
||||
peer.endpoint.get().toString().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if (peer.endpoint.isPresent) {
|
||||
peer.endpoint.get().toString().trim()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
allowedIps = peer.allowedIps.joinToString(", ").trim(),
|
||||
)
|
||||
}
|
||||
|
||||
+71
-114
@@ -1,6 +1,7 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
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.Refresh
|
||||
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.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
@@ -50,6 +51,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
@@ -61,7 +63,6 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
@@ -72,7 +73,6 @@ import androidx.navigation.NavController
|
||||
import com.google.accompanist.drawablepainter.DrawablePainter
|
||||
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.SearchBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||
@@ -86,15 +86,17 @@ import kotlinx.coroutines.delay
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@OptIn(
|
||||
ExperimentalComposeUiApi::class,
|
||||
ExperimentalMaterial3Api::class,
|
||||
ExperimentalFoundationApi::class,
|
||||
)
|
||||
@Composable
|
||||
fun ConfigScreen(
|
||||
viewModel: ConfigViewModel = hiltViewModel(),
|
||||
focusRequester: FocusRequester,
|
||||
navController: NavController,
|
||||
appViewModel: AppViewModel,
|
||||
tunnelId: String
|
||||
showSnackbarMessage: (String) -> Unit,
|
||||
id: String
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
@@ -105,7 +107,7 @@ fun ConfigScreen(
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) { viewModel.init(tunnelId) }
|
||||
LaunchedEffect(Unit) { viewModel.init(id) }
|
||||
|
||||
LaunchedEffect(uiState.loading) {
|
||||
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
@@ -127,17 +129,14 @@ fun ConfigScreen(
|
||||
val fillMaxWidth = .85f
|
||||
val screenPadding = 5.dp
|
||||
|
||||
val applicationButtonText = buildAnnotatedString {
|
||||
append(stringResource(id = R.string.tunneling_apps))
|
||||
append(": ")
|
||||
if (uiState.isAllApplicationsEnabled) {
|
||||
append(stringResource(id = R.string.all))
|
||||
} else {
|
||||
append("${uiState.checkedPackageNames.size} ")
|
||||
(if (uiState.include) append(stringResource(id = R.string.included)) else append(
|
||||
stringResource(id = R.string.excluded),
|
||||
))
|
||||
}
|
||||
val applicationButtonText = {
|
||||
"Tunneling apps: " +
|
||||
if (uiState.isAllApplicationsEnabled) {
|
||||
"all"
|
||||
} else {
|
||||
"${uiState.checkedPackageNames.size} " +
|
||||
(if (uiState.include) "included" else "excluded")
|
||||
}
|
||||
}
|
||||
|
||||
if (showAuthPrompt) {
|
||||
@@ -146,13 +145,13 @@ fun ConfigScreen(
|
||||
showAuthPrompt = false
|
||||
isAuthenticated = true
|
||||
},
|
||||
onError = {
|
||||
onError = { error ->
|
||||
showAuthPrompt = false
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
},
|
||||
onFailure = {
|
||||
showAuthPrompt = false
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -162,26 +161,20 @@ fun ConfigScreen(
|
||||
remember(uiState.packages) {
|
||||
uiState.packages.sortedBy { viewModel.getPackageLabel(it) }
|
||||
}
|
||||
BasicAlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
||||
AlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
||||
Modifier.fillMaxWidth()
|
||||
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
@@ -194,9 +187,8 @@ fun ConfigScreen(
|
||||
if (!uiState.isAllApplicationsEnabled) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
@@ -227,9 +219,8 @@ fun ConfigScreen(
|
||||
}
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
@@ -245,9 +236,7 @@ fun ConfigScreen(
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(5.dp),
|
||||
modifier = Modifier.fillMaxSize().padding(5.dp),
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
|
||||
val drawable =
|
||||
@@ -259,10 +248,9 @@ fun ConfigScreen(
|
||||
modifier = Modifier.size(50.dp, 50.dp),
|
||||
)
|
||||
} else {
|
||||
val icon = Icons.Rounded.Android
|
||||
Icon(
|
||||
icon,
|
||||
icon.name,
|
||||
Icons.Rounded.Android,
|
||||
stringResource(id = R.string.edit),
|
||||
modifier = Modifier.size(50.dp, 50.dp),
|
||||
)
|
||||
}
|
||||
@@ -274,9 +262,9 @@ fun ConfigScreen(
|
||||
Checkbox(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
checked =
|
||||
(uiState.checkedPackageNames.contains(
|
||||
pack.packageName,
|
||||
)),
|
||||
(uiState.checkedPackageNames.contains(
|
||||
pack.packageName
|
||||
)),
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
viewModel.onAddCheckedPackage(pack.packageName)
|
||||
@@ -291,9 +279,7 @@ fun ConfigScreen(
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(onClick = { showApplicationsDialog = false }) {
|
||||
@@ -313,20 +299,19 @@ fun ConfigScreen(
|
||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||
FloatingActionButton(
|
||||
modifier =
|
||||
Modifier.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
},
|
||||
Modifier.padding(bottom = 90.dp).onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
viewModel.onSaveAllChanges().let {
|
||||
when (it) {
|
||||
is Result.Success -> {
|
||||
appViewModel.showSnackbarMessage(it.data.message)
|
||||
showSnackbarMessage(it.data.message)
|
||||
navController.navigate(Screen.Main.route)
|
||||
}
|
||||
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -346,10 +331,7 @@ fun ConfigScreen(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.weight(1f, true)
|
||||
.fillMaxSize(),
|
||||
Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize(),
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
@@ -357,21 +339,17 @@ fun ConfigScreen(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier
|
||||
.fillMaxHeight(fillMaxHeight)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth)
|
||||
})
|
||||
.padding(bottom = 10.dp),
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
||||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth)
|
||||
})
|
||||
.padding(top = 50.dp, bottom = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier
|
||||
.padding(15.dp)
|
||||
.focusGroup(),
|
||||
modifier = Modifier.padding(15.dp).focusGroup(),
|
||||
) {
|
||||
SectionTitle(
|
||||
stringResource(R.string.interface_),
|
||||
@@ -383,20 +361,16 @@ fun ConfigScreen(
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.name),
|
||||
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { showAuthPrompt = true },
|
||||
modifier = Modifier.fillMaxWidth().clickable { showAuthPrompt = true },
|
||||
value = uiState.interfaceProxy.privateKey,
|
||||
visualTransformation =
|
||||
if ((tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
||||
VisualTransformation.None
|
||||
else PasswordVisualTransformation(),
|
||||
enabled = (tunnelId == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
||||
VisualTransformation.None
|
||||
else PasswordVisualTransformation(),
|
||||
enabled = (id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated,
|
||||
onValueChange = { value -> viewModel.onPrivateKeyChange(value) },
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
@@ -418,9 +392,7 @@ fun ConfigScreen(
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(FocusRequester.Default),
|
||||
Modifier.fillMaxWidth().focusRequester(FocusRequester.Default),
|
||||
value = uiState.interfaceProxy.publicKey,
|
||||
enabled = false,
|
||||
onValueChange = {},
|
||||
@@ -453,9 +425,7 @@ fun ConfigScreen(
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.addresses),
|
||||
hint = stringResource(R.string.comma_separated_list),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(3 / 5f)
|
||||
.padding(end = 5.dp),
|
||||
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.listenPort,
|
||||
@@ -473,9 +443,7 @@ fun ConfigScreen(
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.dns_servers),
|
||||
hint = stringResource(R.string.comma_separated_list),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(3 / 5f)
|
||||
.padding(end = 5.dp),
|
||||
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.mtu,
|
||||
@@ -488,13 +456,11 @@ fun ConfigScreen(
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(onClick = { showApplicationsDialog = true }) {
|
||||
Text(applicationButtonText.text)
|
||||
Text(applicationButtonText())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -506,36 +472,29 @@ fun ConfigScreen(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier
|
||||
.fillMaxHeight(fillMaxHeight)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth)
|
||||
})
|
||||
.padding(top = 10.dp, bottom = 10.dp),
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
||||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth)
|
||||
})
|
||||
.padding(top = 10.dp, bottom = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 15.dp)
|
||||
.padding(bottom = 10.dp),
|
||||
modifier = Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 5.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
stringResource(R.string.peer),
|
||||
padding = screenPadding,
|
||||
)
|
||||
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
|
||||
val icon = Icons.Rounded.Delete
|
||||
Icon(icon, icon.name)
|
||||
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,9 +570,7 @@ fun ConfigScreen(
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = 140.dp),
|
||||
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
||||
+52
-47
@@ -14,7 +14,8 @@ import com.wireguard.crypto.Key
|
||||
import com.wireguard.crypto.KeyPair
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
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.PeerProxy
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
@@ -27,8 +28,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -36,7 +37,8 @@ class ConfigViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
private val appDataRepository: AppDataRepository
|
||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val packageManager = application.packageManager
|
||||
@@ -50,8 +52,7 @@ constructor(
|
||||
val state =
|
||||
if (tunnelId != Constants.MANUAL_TUNNEL_CONFIG_ID) {
|
||||
val tunnelConfig =
|
||||
appDataRepository.tunnels.getAll()
|
||||
.firstOrNull { it.id.toString() == tunnelId }
|
||||
tunnelConfigRepository.getAll().firstOrNull { it.id.toString() == tunnelId }
|
||||
if (tunnelConfig != null) {
|
||||
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||
val proxyPeers = config.peers.map { PeerProxy.from(it) }
|
||||
@@ -99,7 +100,7 @@ constructor(
|
||||
fun onAddCheckedPackage(packageName: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
checkedPackageNames = _uiState.value.checkedPackageNames + packageName,
|
||||
checkedPackageNames = _uiState.value.checkedPackageNames + packageName
|
||||
)
|
||||
}
|
||||
|
||||
@@ -110,7 +111,7 @@ constructor(
|
||||
fun onRemoveCheckedPackage(packageName: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
checkedPackageNames = _uiState.value.checkedPackageNames - packageName,
|
||||
checkedPackageNames = _uiState.value.checkedPackageNames - packageName
|
||||
)
|
||||
}
|
||||
|
||||
@@ -144,16 +145,26 @@ constructor(
|
||||
}
|
||||
|
||||
private fun saveConfig(tunnelConfig: TunnelConfig) =
|
||||
viewModelScope.launch { appDataRepository.tunnels.save(tunnelConfig) }
|
||||
viewModelScope.launch { tunnelConfigRepository.save(tunnelConfig) }
|
||||
|
||||
private fun updateTunnelConfig(tunnelConfig: TunnelConfig?) =
|
||||
viewModelScope.launch {
|
||||
if (tunnelConfig != null) {
|
||||
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> {
|
||||
return _uiState.value.proxyPeers.map {
|
||||
val builder = Peer.Builder()
|
||||
@@ -195,12 +206,8 @@ constructor(
|
||||
val peerList = buildPeerListFromProxyPeers()
|
||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||
val tunnelConfig = when (uiState.value.tunnel) {
|
||||
null -> TunnelConfig(
|
||||
name = _uiState.value.tunnelName,
|
||||
wgQuick = config.toWgQuickString(),
|
||||
)
|
||||
|
||||
val tunnelConfig = when(uiState.value.tunnel) {
|
||||
null -> TunnelConfig(name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
|
||||
else -> uiState.value.tunnel!!.copy(
|
||||
name = _uiState.value.tunnelName,
|
||||
wgQuick = config.toWgQuickString(),
|
||||
@@ -209,9 +216,7 @@ constructor(
|
||||
updateTunnelConfig(tunnelConfig)
|
||||
Result.Success(Event.Message.ConfigSaved)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
val message = e.message?.substringAfter(":", missingDelimiterValue = "")
|
||||
Result.Error(Event.Error.ConfigParseError(message ?: ""))
|
||||
Result.Error(Event.Error.Exception(e))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,10 +224,10 @@ constructor(
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(publicKey = value),
|
||||
),
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(publicKey = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -230,10 +235,10 @@ constructor(
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
|
||||
),
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(preSharedKey = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -241,10 +246,10 @@ constructor(
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(endpoint = value),
|
||||
),
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(endpoint = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -252,10 +257,10 @@ constructor(
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(allowedIps = value),
|
||||
),
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(allowedIps = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -263,10 +268,10 @@ constructor(
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
proxyPeers =
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
|
||||
),
|
||||
_uiState.value.proxyPeers.update(
|
||||
index,
|
||||
_uiState.value.proxyPeers[index].copy(persistentKeepalive = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -286,31 +291,31 @@ constructor(
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
interfaceProxy =
|
||||
_uiState.value.interfaceProxy.copy(
|
||||
privateKey = keyPair.privateKey.toBase64(),
|
||||
publicKey = keyPair.publicKey.toBase64(),
|
||||
),
|
||||
_uiState.value.interfaceProxy.copy(
|
||||
privateKey = keyPair.privateKey.toBase64(),
|
||||
publicKey = keyPair.publicKey.toBase64(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onAddressesChanged(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value),
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value)
|
||||
)
|
||||
}
|
||||
|
||||
fun onListenPortChanged(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value),
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value)
|
||||
)
|
||||
}
|
||||
|
||||
fun onDnsServersChanged(value: String) {
|
||||
_uiState.value =
|
||||
_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) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value),
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value)
|
||||
)
|
||||
}
|
||||
|
||||
fun onPrivateKeyChange(value: String) {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value),
|
||||
interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value)
|
||||
)
|
||||
if (NumberUtils.isValidKey(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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.Bolt
|
||||
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.Edit
|
||||
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.material3.AlertDialog
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.typography
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
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.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
@@ -90,7 +93,6 @@ import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
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.handshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
viewModel: MainViewModel = hiltViewModel(),
|
||||
appViewModel: AppViewModel,
|
||||
padding: PaddingValues,
|
||||
focusRequester: FocusRequester,
|
||||
showSnackbarMessage: (String) -> Unit,
|
||||
navController: NavController
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
@@ -124,17 +127,23 @@ fun MainScreen(
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
|
||||
var showPrimaryChangeAlertDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
|
||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
LaunchedEffect(uiState.loading) {
|
||||
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.loading) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val tunnelFileImportResultLauncher =
|
||||
rememberLauncherForActivityResult(
|
||||
object : ActivityResultContracts.GetContent() {
|
||||
@@ -164,7 +173,7 @@ fun MainScreen(
|
||||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||
}
|
||||
) {
|
||||
appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
@@ -174,7 +183,7 @@ fun MainScreen(
|
||||
scope.launch {
|
||||
viewModel.onTunnelFileSelected(data).let {
|
||||
when (it) {
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||
is Result.Success -> {}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +198,7 @@ fun MainScreen(
|
||||
viewModel.onTunnelQrResult(it.contents).let { result ->
|
||||
when (result) {
|
||||
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) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteTunnelAlertDialog = false },
|
||||
@@ -222,25 +255,63 @@ fun MainScreen(
|
||||
}
|
||||
|
||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||
if (appViewModel.isRequiredPermissionGranted()) {
|
||||
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.loading) {
|
||||
return LoadingScreen()
|
||||
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier =
|
||||
Modifier.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
selectedTunnel = null
|
||||
},
|
||||
)
|
||||
},
|
||||
Modifier.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null
|
||||
},
|
||||
)
|
||||
},
|
||||
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 = {
|
||||
AnimatedVisibility(
|
||||
visible = isVisible.value,
|
||||
@@ -252,17 +323,18 @@ fun MainScreen(
|
||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||
FloatingActionButton(
|
||||
modifier =
|
||||
(if (
|
||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||
uiState.tunnels.isEmpty()
|
||||
)
|
||||
Modifier.focusRequester(focusRequester)
|
||||
else Modifier)
|
||||
.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
},
|
||||
(if (
|
||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||
uiState.tunnels.isEmpty()
|
||||
)
|
||||
Modifier.focusRequester(focusRequester)
|
||||
else Modifier)
|
||||
.padding(bottom = 90.dp)
|
||||
.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
},
|
||||
onClick = { showBottomSheet = true },
|
||||
containerColor = fobColor,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
@@ -275,13 +347,12 @@ fun MainScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
) { innerPadding ->
|
||||
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
) {
|
||||
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
@@ -294,13 +365,12 @@ fun MainScreen(
|
||||
// Sheet content
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
showBottomSheet = false
|
||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||
}
|
||||
.padding(10.dp),
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable {
|
||||
showBottomSheet = false
|
||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||
}
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.FileOpen,
|
||||
@@ -313,27 +383,26 @@ fun MainScreen(
|
||||
)
|
||||
}
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
HorizontalDivider()
|
||||
Divider()
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
scope.launch {
|
||||
showBottomSheet = false
|
||||
val scanOptions = ScanOptions()
|
||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||
scanOptions.setOrientationLocked(true)
|
||||
scanOptions.setPrompt(
|
||||
context.getString(R.string.scanning_qr),
|
||||
)
|
||||
scanOptions.setBeepEnabled(false)
|
||||
scanOptions.captureActivity =
|
||||
CaptureActivityPortrait::class.java
|
||||
scanLauncher.launch(scanOptions)
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable {
|
||||
scope.launch {
|
||||
showBottomSheet = false
|
||||
val scanOptions = ScanOptions()
|
||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||
scanOptions.setOrientationLocked(true)
|
||||
scanOptions.setPrompt(
|
||||
context.getString(R.string.scanning_qr)
|
||||
)
|
||||
scanOptions.setBeepEnabled(false)
|
||||
scanOptions.captureActivity =
|
||||
CaptureActivityPortrait::class.java
|
||||
scanLauncher.launch(scanOptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(10.dp),
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.QrCode,
|
||||
@@ -346,18 +415,17 @@ fun MainScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
Divider()
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
showBottomSheet = false
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
||||
)
|
||||
}
|
||||
.padding(10.dp),
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable {
|
||||
showBottomSheet = false
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
||||
)
|
||||
}
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Create,
|
||||
@@ -376,70 +444,23 @@ fun MainScreen(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.overscroll(ScrollableDefaults.overscrollEffect()),
|
||||
Modifier.fillMaxWidth()
|
||||
.fillMaxHeight(.90f)
|
||||
.overscroll(ScrollableDefaults.overscrollEffect())
|
||||
.padding(innerPadding),
|
||||
state = rememberLazyListState(0, uiState.tunnels.count()),
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = false,
|
||||
reverseLayout = true,
|
||||
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(
|
||||
uiState.tunnels,
|
||||
key = { tunnel -> tunnel.id },
|
||||
) { tunnel ->
|
||||
val leadingIconColor =
|
||||
(if (
|
||||
uiState.vpnState.tunnelConfig?.name == tunnel.name &&
|
||||
uiState.vpnState.status == Tunnel.State.UP
|
||||
uiState.vpnState.name == tunnel.name &&
|
||||
uiState.vpnState.status == Tunnel.State.UP
|
||||
) {
|
||||
uiState.vpnState.statistics
|
||||
?.mapPeerStats()
|
||||
@@ -450,7 +471,6 @@ fun MainScreen(
|
||||
statuses?.any { it == HandshakeStatus.STALE } == true -> corn
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
|
||||
Color.Gray
|
||||
|
||||
else -> {
|
||||
Color.Gray
|
||||
}
|
||||
@@ -462,33 +482,29 @@ fun MainScreen(
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
RowListItem(
|
||||
icon = {
|
||||
val circleIcon = Icons.Rounded.Circle
|
||||
val icon = if (tunnel.isPrimaryTunnel) {
|
||||
Icons.Rounded.Star
|
||||
} else if (tunnel.isMobileDataTunnel) {
|
||||
Icons.Rounded.Smartphone
|
||||
if (uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||
Icon(
|
||||
Icons.Rounded.Star,
|
||||
stringResource(R.string.status),
|
||||
tint = leadingIconColor,
|
||||
modifier = Modifier.padding(end = 10.dp).size(20.dp),
|
||||
)
|
||||
} 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 = {
|
||||
if (
|
||||
(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
|
||||
}
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
@@ -498,7 +514,7 @@ fun MainScreen(
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (
|
||||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
||||
(uiState.vpnState.name == tunnel.name)
|
||||
) {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
@@ -512,51 +528,62 @@ fun MainScreen(
|
||||
rowButton = {
|
||||
if (
|
||||
tunnel.id == selectedTunnel?.id &&
|
||||
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||
) {
|
||||
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(
|
||||
onClick = {
|
||||
if (
|
||||
uiState.settings.isAutoTunnelEnabled &&
|
||||
!uiState.settings.isAutoTunnelPaused
|
||||
uiState.settings.isTunnelConfigDefault(
|
||||
tunnel,
|
||||
) &&
|
||||
!uiState.settings.isAutoTunnelPaused
|
||||
) {
|
||||
appViewModel.showSnackbarMessage(
|
||||
showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
)
|
||||
} else {
|
||||
} else
|
||||
navController.navigate(
|
||||
"${Screen.Option.route}/${selectedTunnel?.id}",
|
||||
"${Screen.Config.route}/${selectedTunnel?.id}",
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
val icon = Icons.Rounded.Settings
|
||||
Icon(
|
||||
icon,
|
||||
icon.name,
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
modifier = Modifier.focusable(),
|
||||
onClick = { viewModel.onCopyTunnel(selectedTunnel) },
|
||||
) {
|
||||
val icon = Icons.Rounded.CopyAll
|
||||
Icon(icon, icon.name)
|
||||
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
||||
}
|
||||
IconButton(
|
||||
modifier = Modifier.focusable(),
|
||||
onClick = { showDeleteTunnelAlertDialog = true },
|
||||
) {
|
||||
val icon = Icons.Rounded.Delete
|
||||
Icon(icon, icon.name)
|
||||
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val checked by remember {
|
||||
derivedStateOf {
|
||||
(uiState.vpnState.status == Tunnel.State.UP &&
|
||||
tunnel.name == uiState.vpnState.tunnelConfig?.name)
|
||||
tunnel.name == uiState.vpnState.name)
|
||||
}
|
||||
}
|
||||
if (!checked) expanded.value = false
|
||||
@@ -573,69 +600,77 @@ fun MainScreen(
|
||||
)
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Row {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (uiState.settings.isAutoTunnelEnabled) {
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
)
|
||||
} else {
|
||||
selectedTunnel = tunnel
|
||||
navController.navigate(
|
||||
"${Screen.Option.route}/${selectedTunnel?.id}",
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
val icon = Icons.Rounded.Settings
|
||||
Icon(
|
||||
icon,
|
||||
icon.name,
|
||||
)
|
||||
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (uiState.settings.isAutoTunnelEnabled) {
|
||||
showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
)
|
||||
} else {
|
||||
selectedTunnel = tunnel
|
||||
showPrimaryChangeAlertDialog = true
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.Star,
|
||||
stringResource(id = R.string.set_primary),
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
onClick = {
|
||||
if (
|
||||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
||||
(uiState.vpnState.name == tunnel.name)
|
||||
) {
|
||||
expanded.value = !expanded.value
|
||||
} else {
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.TunnelOnAction.message,
|
||||
showSnackbarMessage(
|
||||
Event.Message.TunnelOnAction.message
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
val icon = Icons.Rounded.Info
|
||||
Icon(icon, icon.name)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { viewModel.onCopyTunnel(tunnel) },
|
||||
) {
|
||||
val icon = Icons.Rounded.CopyAll
|
||||
Icon(icon, icon.name)
|
||||
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (
|
||||
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
|
||||
)
|
||||
} 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 {
|
||||
selectedTunnel = tunnel
|
||||
showDeleteTunnelAlertDialog = true
|
||||
}
|
||||
},
|
||||
) {
|
||||
val icon = Icons.Rounded.Delete
|
||||
Icon(
|
||||
icon,
|
||||
icon.name,
|
||||
Icons.Rounded.Delete,
|
||||
stringResource(id = R.string.delete),
|
||||
)
|
||||
}
|
||||
TunnelSwitch()
|
||||
|
||||
+57
-55
@@ -11,7 +11,8 @@ import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
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.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
@@ -20,6 +21,8 @@ import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -35,67 +38,76 @@ class MainViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
val vpnService: VpnService
|
||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vpnService: VpnService
|
||||
) : ViewModel() {
|
||||
|
||||
val uiState =
|
||||
combine(
|
||||
appDataRepository.settings.getSettingsFlow(),
|
||||
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
||||
vpnService.vpnState,
|
||||
) { settings, tunnels, vpnState ->
|
||||
MainUiState(settings, tunnels, vpnState, false)
|
||||
}
|
||||
settingsRepository.getSettingsFlow(),
|
||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||
vpnService.vpnState,
|
||||
) { settings, tunnels, vpnState ->
|
||||
validateWatcherServiceState(settings)
|
||||
MainUiState(settings, tunnels, vpnState, false)
|
||||
}
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||
MainUiState(),
|
||||
)
|
||||
|
||||
private fun validateWatcherServiceState(settings: Settings) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
ServiceManager.startWatcherService(application.applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopWatcherService() =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
serviceManager.stopWatcherService(application.applicationContext)
|
||||
ServiceManager.stopWatcherService(application.applicationContext)
|
||||
}
|
||||
|
||||
fun onDelete(tunnel: TunnelConfig) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val settings = appDataRepository.settings.getSettings()
|
||||
val isPrimary = tunnel.isPrimaryTunnel
|
||||
if (appDataRepository.tunnels.count() == 1 || isPrimary) {
|
||||
if (tunnelConfigRepository.count() == 1) {
|
||||
stopWatcherService()
|
||||
resetTunnelSetting(settings)
|
||||
val settings = settingsRepository.getSettings()
|
||||
settings.defaultTunnel = null
|
||||
settings.isAutoTunnelEnabled = false
|
||||
settings.isAlwaysOnVpnEnabled = false
|
||||
saveSettings(settings)
|
||||
}
|
||||
appDataRepository.tunnels.delete(tunnel)
|
||||
WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||
tunnelConfigRepository.delete(tunnel)
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetTunnelSetting(settings: Settings) {
|
||||
saveSettings(
|
||||
settings.copy(
|
||||
isAutoTunnelEnabled = false,
|
||||
isAlwaysOnVpnEnabled = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onTunnelStart(tunnelConfig: TunnelConfig) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Timber.d("On start called!")
|
||||
serviceManager.startVpnService(
|
||||
application.applicationContext,
|
||||
tunnelConfig.id,
|
||||
isManualStart = true,
|
||||
)
|
||||
stopActiveTunnel().await()
|
||||
startTunnel(tunnelConfig)
|
||||
}
|
||||
|
||||
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() =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Timber.i("Stopping active tunnel")
|
||||
serviceManager.stopVpnService(application.applicationContext, isManualStop = true)
|
||||
Timber.d("Stopping active tunnel")
|
||||
ServiceManager.stopVpnService(application.applicationContext)
|
||||
}
|
||||
|
||||
private fun validateConfigString(config: String) {
|
||||
@@ -110,7 +122,6 @@ constructor(
|
||||
addTunnel(tunnelConfig)
|
||||
Result.Success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Result.Error(Event.Error.InvalidQrCode)
|
||||
}
|
||||
}
|
||||
@@ -139,7 +150,6 @@ constructor(
|
||||
is Result.Success -> return it
|
||||
}
|
||||
}
|
||||
|
||||
Constants.ZIP_FILE_EXTENSION -> saveTunnelsFromZipUri(uri)
|
||||
else -> return Result.Error(Event.Error.InvalidFileExtension)
|
||||
}
|
||||
@@ -148,7 +158,6 @@ constructor(
|
||||
return Result.Error(Event.Error.InvalidFileExtension)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return Result.Error(Event.Error.FileReadFailed)
|
||||
}
|
||||
}
|
||||
@@ -181,25 +190,22 @@ constructor(
|
||||
}
|
||||
|
||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||
val firstTunnel = appDataRepository.tunnels.count() == 0
|
||||
saveTunnel(tunnelConfig)
|
||||
if (firstTunnel) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(application)
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
}
|
||||
|
||||
fun pauseAutoTunneling() =
|
||||
viewModelScope.launch {
|
||||
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = true))
|
||||
}
|
||||
|
||||
fun resumeAutoTunneling() =
|
||||
viewModelScope.launch {
|
||||
appDataRepository.settings.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||
settingsRepository.save(uiState.value.settings.copy(isAutoTunnelPaused = false))
|
||||
}
|
||||
|
||||
private suspend fun saveTunnel(tunnelConfig: TunnelConfig) {
|
||||
appDataRepository.tunnels.save(tunnelConfig)
|
||||
tunnelConfigRepository.save(tunnelConfig)
|
||||
}
|
||||
|
||||
private fun getFileNameByCursor(context: Context, uri: Uri): String? {
|
||||
@@ -243,23 +249,19 @@ constructor(
|
||||
return try {
|
||||
fileName.substring(fileName.lastIndexOf('.'))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveSettings(settings: Settings) =
|
||||
viewModelScope.launch(Dispatchers.IO) { appDataRepository.settings.save(settings) }
|
||||
viewModelScope.launch(Dispatchers.IO) { settingsRepository.save(settings) }
|
||||
|
||||
|
||||
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
|
||||
tunnel?.let {
|
||||
saveTunnel(
|
||||
TunnelConfig(
|
||||
name = it.name.plus(NumberUtils.randomThree()),
|
||||
wgQuick = it.wgQuick,
|
||||
),
|
||||
)
|
||||
fun onDefaultTunnelChange(selectedTunnel: TunnelConfig?) =
|
||||
viewModelScope.launch {
|
||||
if (selectedTunnel != null) {
|
||||
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
|
||||
.join()
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-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.FlowRow
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -53,6 +55,7 @@ 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.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -66,7 +69,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
@@ -74,19 +76,16 @@ import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
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.prompt.AuthorizationPrompt
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import java.io.File
|
||||
|
||||
@OptIn(
|
||||
@@ -96,16 +95,15 @@ import java.io.File
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
viewModel: SettingsViewModel = hiltViewModel(),
|
||||
appViewModel: AppViewModel,
|
||||
navController: NavController,
|
||||
focusRequester: FocusRequester,
|
||||
padding: PaddingValues,
|
||||
showSnackbarMessage: (String) -> Unit,
|
||||
focusRequester: FocusRequester
|
||||
) {
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val scrollState = rememberScrollState()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -115,14 +113,21 @@ fun SettingsScreen(
|
||||
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||
var didExportFiles by remember { mutableStateOf(false) }
|
||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||
val focusRequester2 = remember { FocusRequester() }
|
||||
|
||||
val screenPadding = 5.dp
|
||||
val fillMaxWidth = .85f
|
||||
|
||||
if (uiState.loading) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val startForResult =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
result.data
|
||||
val intent = result.data
|
||||
// Handle the Intent
|
||||
}
|
||||
viewModel.setBatteryOptimizeDisableShown()
|
||||
@@ -136,10 +141,9 @@ fun SettingsScreen(
|
||||
}
|
||||
FileUtils.saveFilesToZip(context, files)
|
||||
didExportFiles = true
|
||||
appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||
showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
appViewModel.showSnackbarMessage(Event.Error.Exception(e).message)
|
||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,9 +163,7 @@ fun SettingsScreen(
|
||||
|
||||
fun handleAutoTunnelToggle() {
|
||||
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
||||
if (appViewModel.isRequiredPermissionGranted()) {
|
||||
viewModel.onToggleAutoTunnel()
|
||||
}
|
||||
viewModel.toggleAutoTunnel()
|
||||
} else {
|
||||
requestBatteryOptimizationsDisabled()
|
||||
}
|
||||
@@ -172,7 +174,7 @@ fun SettingsScreen(
|
||||
viewModel.onSaveTrustedSSID(currentText).let {
|
||||
when (it) {
|
||||
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 (
|
||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
||||
) {
|
||||
checkFineLocationGranted()
|
||||
} else {
|
||||
@@ -246,16 +248,12 @@ fun SettingsScreen(
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState),
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.LocationOff,
|
||||
contentDescription = stringResource(id = R.string.map),
|
||||
modifier = Modifier
|
||||
.padding(30.dp)
|
||||
.size(128.dp),
|
||||
modifier = Modifier.padding(30.dp).size(128.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_title),
|
||||
@@ -271,15 +269,11 @@ fun SettingsScreen(
|
||||
)
|
||||
Row(
|
||||
modifier =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(30.dp)
|
||||
},
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier.fillMaxWidth().padding(10.dp)
|
||||
} else {
|
||||
Modifier.fillMaxWidth().padding(30.dp)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
@@ -307,11 +301,11 @@ fun SettingsScreen(
|
||||
},
|
||||
onError = { _ ->
|
||||
showAuthPrompt = false
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
},
|
||||
onFailure = {
|
||||
showAuthPrompt = false
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -320,7 +314,7 @@ fun SettingsScreen(
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.one_tunnel_required),
|
||||
@@ -335,10 +329,7 @@ fun SettingsScreen(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.clickable(
|
||||
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
|
||||
indication = null,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
@@ -351,17 +342,14 @@ fun SettingsScreen(
|
||||
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),
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp)
|
||||
})
|
||||
.padding(bottom = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -375,43 +363,38 @@ fun SettingsScreen(
|
||||
ConfigurationToggle(
|
||||
stringResource(id = R.string.tunnel_on_wifi),
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
||||
modifier =
|
||||
if (uiState.settings.isAutoTunnelEnabled) Modifier
|
||||
else
|
||||
Modifier
|
||||
.focusRequester(focusRequester),
|
||||
if (uiState.settings.isAutoTunnelEnabled) Modifier
|
||||
else
|
||||
Modifier.focusRequester(focusRequester).focusProperties {
|
||||
down = focusRequester2
|
||||
},
|
||||
)
|
||||
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
||||
Column {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.padding(screenPadding)
|
||||
.fillMaxWidth(),
|
||||
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
) {
|
||||
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
||||
ClickableIconButton(
|
||||
onClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
focusRequester.requestFocus()
|
||||
viewModel.onDeleteTrustedSSID(ssid)
|
||||
focusRequester2.requestFocus()
|
||||
}
|
||||
},
|
||||
onIconClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
|
||||
viewModel.onDeleteTrustedSSID(ssid)
|
||||
|
||||
},
|
||||
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
|
||||
text = ssid,
|
||||
icon = Icons.Filled.Close,
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
)
|
||||
}
|
||||
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
||||
@@ -424,24 +407,24 @@ fun SettingsScreen(
|
||||
}
|
||||
OutlinedTextField(
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
value = currentText,
|
||||
onValueChange = { currentText = it },
|
||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
start = screenPadding,
|
||||
top = 5.dp,
|
||||
bottom = 10.dp,
|
||||
),
|
||||
Modifier.padding(
|
||||
start = screenPadding,
|
||||
top = 5.dp,
|
||||
bottom = 10.dp,
|
||||
)
|
||||
.focusRequester(focusRequester2),
|
||||
maxLines = 1,
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||
trailingIcon = {
|
||||
if (currentText != "") {
|
||||
@@ -449,19 +432,19 @@ fun SettingsScreen(
|
||||
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,
|
||||
)
|
||||
},
|
||||
if (currentText == "") {
|
||||
stringResource(
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_empty_description,
|
||||
)
|
||||
} else {
|
||||
stringResource(
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_value_description,
|
||||
)
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
@@ -473,8 +456,8 @@ fun SettingsScreen(
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.tunnel_mobile_data),
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
||||
@@ -482,31 +465,31 @@ fun SettingsScreen(
|
||||
ConfigurationToggle(
|
||||
stringResource(id = R.string.tunnel_on_ethernet),
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.restart_on_ping),
|
||||
stringResource(R.string.battery_saver),
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isPingEnabled,
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isBatterySaverEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleRestartOnPing() },
|
||||
onCheckChanged = { viewModel.onToggleBatterySaver() },
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
||||
else
|
||||
Modifier.focusRequester(
|
||||
focusRequester,
|
||||
))
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
||||
else
|
||||
Modifier.focusRequester(
|
||||
focusRequester,
|
||||
))
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(
|
||||
@@ -514,22 +497,19 @@ fun SettingsScreen(
|
||||
onClick = {
|
||||
if (
|
||||
uiState.settings.isTunnelOnWifiEnabled &&
|
||||
!uiState.settings.isAutoTunnelEnabled
|
||||
!uiState.settings.isAutoTunnelEnabled
|
||||
) {
|
||||
when (false) {
|
||||
isBackgroundLocationGranted ->
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Error.BackgroundLocationRequired.message,
|
||||
showSnackbarMessage(
|
||||
Event.Error.BackgroundLocationRequired.message
|
||||
)
|
||||
|
||||
fineLocationState.status.isGranted ->
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Error.PreciseLocationRequired.message,
|
||||
showSnackbarMessage(
|
||||
Event.Error.PreciseLocationRequired.message
|
||||
)
|
||||
|
||||
viewModel.isLocationEnabled(context) ->
|
||||
showLocationServicesAlertDialog = true
|
||||
|
||||
else -> {
|
||||
handleAutoTunnelToggle()
|
||||
}
|
||||
@@ -556,9 +536,7 @@ fun SettingsScreen(
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(vertical = 10.dp),
|
||||
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -572,15 +550,15 @@ fun SettingsScreen(
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.use_kernel),
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||
checked = uiState.settings.isKernelEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
viewModel.onToggleKernelMode().let {
|
||||
when (it) {
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||
is Result.Success -> {}
|
||||
}
|
||||
}
|
||||
@@ -589,27 +567,26 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(vertical = 10.dp)
|
||||
.padding(bottom = 140.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth(fillMaxWidth)
|
||||
.padding(vertical = 10.dp)
|
||||
.padding(bottom = 140.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.other),
|
||||
padding = screenPadding,
|
||||
)
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.other),
|
||||
padding = screenPadding,
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.always_on_vpn_support),
|
||||
enabled = !uiState.settings.isAutoTunnelEnabled,
|
||||
@@ -624,27 +601,9 @@ fun SettingsScreen(
|
||||
padding = screenPadding,
|
||||
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(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
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 vpnState: VpnState = VpnState(),
|
||||
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.viewModelScope
|
||||
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.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.tunnel.VpnService
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
@@ -28,27 +29,29 @@ class SettingsViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
private val tunnelConfigRepository: TunnelConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val dataStoreManager: DataStoreManager,
|
||||
private val rootShell: RootShell,
|
||||
vpnService: VpnService
|
||||
private val vpnService: VpnService
|
||||
) : ViewModel() {
|
||||
|
||||
val uiState =
|
||||
combine(
|
||||
appDataRepository.settings.getSettingsFlow(),
|
||||
appDataRepository.tunnels.getTunnelConfigsFlow(),
|
||||
vpnService.vpnState,
|
||||
appDataRepository.appState.generalStateFlow,
|
||||
) { settings, tunnels, tunnelState, generalState ->
|
||||
SettingsUiState(
|
||||
settings,
|
||||
tunnels,
|
||||
tunnelState,
|
||||
generalState.locationDisclosureShown,
|
||||
generalState.batteryOptimizationDisableShown,
|
||||
)
|
||||
}
|
||||
settingsRepository.getSettingsFlow(),
|
||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||
vpnService.vpnState,
|
||||
dataStoreManager.preferencesFlow,
|
||||
) { settings, tunnels, tunnelState, preferences ->
|
||||
SettingsUiState(
|
||||
settings,
|
||||
tunnels,
|
||||
tunnelState,
|
||||
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
|
||||
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
|
||||
false
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||
@@ -68,12 +71,12 @@ constructor(
|
||||
|
||||
fun setLocationDisclosureShown() =
|
||||
viewModelScope.launch {
|
||||
appDataRepository.appState.setLocationDisclosureShown(true)
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, true)
|
||||
}
|
||||
|
||||
fun setBatteryOptimizeDisableShown() =
|
||||
viewModelScope.launch {
|
||||
appDataRepository.appState.setBatteryOptimizationDisableShown(true)
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, true)
|
||||
}
|
||||
|
||||
fun onToggleTunnelOnMobileData() {
|
||||
@@ -88,42 +91,48 @@ constructor(
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
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 {
|
||||
val isAutoTunnelEnabled = uiState.value.settings.isAutoTunnelEnabled
|
||||
var isAutoTunnelPaused = uiState.value.settings.isAutoTunnelPaused
|
||||
|
||||
if (isAutoTunnelEnabled) {
|
||||
serviceManager.stopWatcherService(application)
|
||||
ServiceManager.stopWatcherService(application)
|
||||
} else {
|
||||
serviceManager.startWatcherService(application)
|
||||
ServiceManager.startWatcherService(application)
|
||||
isAutoTunnelPaused = false
|
||||
}
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isAutoTunnelEnabled = !isAutoTunnelEnabled,
|
||||
isAutoTunnelPaused = isAutoTunnelPaused,
|
||||
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||
),
|
||||
)
|
||||
WireGuardAutoTunnel.requestAutoTunnelTileServiceUpdate(application)
|
||||
}
|
||||
|
||||
fun onToggleAlwaysOnVPN() =
|
||||
viewModelScope.launch {
|
||||
saveSettings(
|
||||
val updatedSettings =
|
||||
uiState.value.settings.copy(
|
||||
isAlwaysOnVpnEnabled = !uiState.value.settings.isAlwaysOnVpnEnabled,
|
||||
),
|
||||
)
|
||||
defaultTunnel = getDefaultTunnelOrFirst(),
|
||||
)
|
||||
saveSettings(updatedSettings)
|
||||
}
|
||||
|
||||
private fun saveSettings(settings: Settings) =
|
||||
viewModelScope.launch { appDataRepository.settings.save(settings) }
|
||||
viewModelScope.launch { settingsRepository.save(settings) }
|
||||
|
||||
fun onToggleTunnelOnEthernet() {
|
||||
saveSettings(
|
||||
@@ -146,6 +155,14 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun onToggleBatterySaver() {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
isBatterySaverEnabled = !uiState.value.settings.isBatterySaverEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveKernelMode(on: Boolean) {
|
||||
saveSettings(
|
||||
uiState.value.settings.copy(
|
||||
@@ -166,10 +183,9 @@ constructor(
|
||||
if (!uiState.value.settings.isKernelEnabled) {
|
||||
try {
|
||||
rootShell.start()
|
||||
Timber.i("Root shell accepted!")
|
||||
Timber.d("Root shell accepted!")
|
||||
saveKernelMode(on = true)
|
||||
} catch (e: RootShell.RootShellException) {
|
||||
Timber.e(e)
|
||||
saveKernelMode(on = false)
|
||||
return Result.Error(Event.Error.RootDenied)
|
||||
}
|
||||
@@ -178,12 +194,4 @@ constructor(
|
||||
}
|
||||
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
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.Intent.createChooser
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.verticalScroll
|
||||
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.FormatListNumbered
|
||||
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.MaterialTheme
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
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.screen.LoadingScreen
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
|
||||
@Composable
|
||||
fun SupportScreen(
|
||||
viewModel: SupportViewModel = hiltViewModel(),
|
||||
appViewModel: AppViewModel,
|
||||
navController: NavController,
|
||||
padding: PaddingValues,
|
||||
showSnackbarMessage: (String) -> Unit,
|
||||
focusRequester: FocusRequester
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -63,14 +67,47 @@ fun SupportScreen(
|
||||
|
||||
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(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.focusable(),
|
||||
Modifier.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.focusable()
|
||||
.padding(padding),
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
@@ -78,20 +115,16 @@ fun SupportScreen(
|
||||
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 = 25.dp),
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
||||
})
|
||||
.padding(bottom = 25.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(20.dp)) {
|
||||
val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward
|
||||
Text(
|
||||
stringResource(R.string.thank_you),
|
||||
textAlign = TextAlign.Start,
|
||||
@@ -106,10 +139,8 @@ fun SupportScreen(
|
||||
modifier = Modifier.padding(bottom = 20.dp),
|
||||
)
|
||||
TextButton(
|
||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||
modifier = Modifier
|
||||
.padding(vertical = 5.dp)
|
||||
.focusRequester(focusRequester),
|
||||
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -117,26 +148,19 @@ fun SupportScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
val icon = Icons.Rounded.Book
|
||||
Icon(icon, icon.name)
|
||||
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
||||
Text(
|
||||
stringResource(id = R.string.docs_description),
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
forwardIcon,
|
||||
forwardIcon.name,
|
||||
)
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
}
|
||||
}
|
||||
HorizontalDivider(
|
||||
thickness = 0.5.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||
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),
|
||||
) {
|
||||
Row(
|
||||
@@ -145,10 +169,9 @@ fun SupportScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
val icon = ImageVector.vectorResource(R.drawable.discord)
|
||||
Icon(
|
||||
icon,
|
||||
icon.name,
|
||||
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
||||
stringResource(id = R.string.discord),
|
||||
Modifier.size(25.dp),
|
||||
)
|
||||
Text(
|
||||
@@ -157,18 +180,12 @@ fun SupportScreen(
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
forwardIcon,
|
||||
forwardIcon.name,
|
||||
)
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
}
|
||||
}
|
||||
HorizontalDivider(
|
||||
thickness = 0.5.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||
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),
|
||||
) {
|
||||
Row(
|
||||
@@ -177,30 +194,23 @@ fun SupportScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
val icon = ImageVector.vectorResource(R.drawable.github)
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
icon.name,
|
||||
imageVector = ImageVector.vectorResource(R.drawable.github),
|
||||
stringResource(id = R.string.github),
|
||||
Modifier.size(25.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.open_issue),
|
||||
"Open an issue",
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
forwardIcon,
|
||||
forwardIcon.name,
|
||||
)
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
}
|
||||
}
|
||||
HorizontalDivider(
|
||||
thickness = 0.5.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
||||
TextButton(
|
||||
onClick = { appViewModel.launchEmail() },
|
||||
onClick = { launchEmail() },
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
@@ -209,48 +219,14 @@ fun SupportScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
val icon = Icons.Rounded.Mail
|
||||
Icon(icon, icon.name)
|
||||
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
||||
Text(
|
||||
stringResource(id = R.string.email_description),
|
||||
textAlign = TextAlign.Justify,
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
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),
|
||||
)
|
||||
}
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -261,9 +237,9 @@ fun SupportScreen(
|
||||
style = TextStyle(textDecoration = TextDecoration.Underline),
|
||||
fontSize = 16.sp,
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||
},
|
||||
Modifier.clickable {
|
||||
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||
},
|
||||
)
|
||||
Row(
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@HiltViewModel
|
||||
class SupportViewModel @Inject constructor(settingsRepository: SettingsRepository) :
|
||||
class SupportViewModel @Inject constructor(private val settingsRepository: SettingsRepository) :
|
||||
ViewModel() {
|
||||
|
||||
val uiState =
|
||||
settingsRepository
|
||||
.getSettingsFlow()
|
||||
.map { SupportUiState(it) }
|
||||
.map { SupportUiState(it, false) }
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
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
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
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 =
|
||||
Typography(
|
||||
bodyLarge =
|
||||
TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp,
|
||||
),
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
|
||||
@@ -1,38 +1,22 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
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 BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
|
||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
|
||||
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
|
||||
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes
|
||||
const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1000L // 30 minutes
|
||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
||||
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3000L
|
||||
const val TOGGLE_TUNNEL_DELAY = 300L
|
||||
const val WATCHER_COLLECTION_DELAY = 1_000L
|
||||
const val CONF_FILE_EXTENSION = ".conf"
|
||||
const val ZIP_FILE_EXTENSION = ".zip"
|
||||
const val URI_CONTENT_SCHEME = "content"
|
||||
const val URI_PACKAGE_SCHEME = "package"
|
||||
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 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 SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024
|
||||
|
||||
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
||||
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)
|
||||
}
|
||||
|
||||
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() {
|
||||
override val message: String
|
||||
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 {
|
||||
val df = DecimalFormat("#.###")
|
||||
return df.format(this)
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.MediaStore.MediaColumns
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.util.zip.ZipEntry
|
||||
@@ -16,7 +15,6 @@ import java.util.zip.ZipOutputStream
|
||||
object FileUtils {
|
||||
private const val ZIP_FILE_MIME_TYPE = "application/zip"
|
||||
|
||||
//TODO issue with android 9
|
||||
private fun createDownloadsFileOutputStream(
|
||||
context: Context,
|
||||
fileName: String,
|
||||
@@ -45,31 +43,6 @@ object FileUtils {
|
||||
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>) {
|
||||
val zipOutputStream =
|
||||
createDownloadsFileOutputStream(
|
||||
|
||||
@@ -19,15 +19,7 @@ object NumberUtils {
|
||||
}
|
||||
|
||||
fun generateRandomTunnelName(): String {
|
||||
return "tunnel${randomFive()}"
|
||||
}
|
||||
|
||||
private fun randomFive(): Int {
|
||||
return (Math.random() * 100000).toInt()
|
||||
}
|
||||
|
||||
fun randomThree(): Int {
|
||||
return (Math.random() * 1000).toInt()
|
||||
return "tunnel${(Math.random() * 100000).toInt()}"
|
||||
}
|
||||
|
||||
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: 4.2 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user