Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5946d7c10d | |||
| 4fc8ffbcbb | |||
| c0cff297b2 | |||
| ee8db0a859 | |||
| c8205c4c59 | |||
| 3247e94358 | |||
| 2690ce29e1 | |||
| 500b85f687 | |||
| 84b2b75271 | |||
| 0197198f7b | |||
| 0b271778c9 | |||
| 6427b2f832 | |||
| 097097f620 | |||
| 20dfaed8de | |||
| 07aa37fc2a | |||
| 091cd2e028 | |||
| 7efa6d0bf4 | |||
| e31fb01410 | |||
| 76674323e7 | |||
| f1fc9ca6f7 | |||
| cb301e74eb | |||
| 8141fe19be | |||
| 0fdb3d0b31 | |||
| d9f3a21cc3 | |||
| fec84bc6ac | |||
| d6ee36edc0 | |||
| e3fcf712d5 | |||
| 3339448424 |
@@ -0,0 +1,2 @@
|
|||||||
|
ko_fi: zaneschepke
|
||||||
|
liberapay: zaneschepke
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
# name of the workflow
|
# name of the workflow
|
||||||
name: Android CI Tag Deployment
|
name: Android CI Tag Deployment (Pre-release)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*.*.*'
|
- '*.*.*-**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -13,10 +14,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
env:
|
env:
|
||||||
KEY_STORE_PATH: ${{ secrets.KEY_STORE_PATH }}
|
|
||||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||||
|
KEY_STORE_FILE: 'android_keystore.jks'
|
||||||
|
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
||||||
|
GH_USER: ${{ secrets.GH_USER }}
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -35,10 +39,16 @@ jobs:
|
|||||||
id: decode_keystore
|
id: decode_keystore
|
||||||
uses: timheuer/base64-to-file@v1.2
|
uses: timheuer/base64-to-file@v1.2
|
||||||
with:
|
with:
|
||||||
fileName: 'android_keystore.jks'
|
fileName: ${{ env.KEY_STORE_FILE }}
|
||||||
fileDir: ${{ github.workspace }}/app/keystore/
|
fileDir: ${{ env.KEY_STORE_LOCATION }}
|
||||||
encodedString: ${{ secrets.KEYSTORE }}
|
encodedString: ${{ secrets.KEYSTORE }}
|
||||||
|
|
||||||
|
# create keystore path for gradle to read
|
||||||
|
- name: Create keystore path env var
|
||||||
|
run: |
|
||||||
|
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
|
||||||
|
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Create service_account.json
|
- name: Create service_account.json
|
||||||
id: createServiceAccount
|
id: createServiceAccount
|
||||||
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||||
@@ -52,10 +62,13 @@ jobs:
|
|||||||
- name: Get apk path
|
- name: Get apk path
|
||||||
id: apk-path
|
id: apk-path
|
||||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
||||||
|
- name: Get version code
|
||||||
|
run: |
|
||||||
|
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
|
||||||
|
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
|
||||||
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v4.0.0
|
uses: actions/upload-artifact@v4.3.1
|
||||||
with:
|
with:
|
||||||
name: wgtunnel
|
name: wgtunnel
|
||||||
path: ${{ steps.apk-path.outputs.path }}
|
path: ${{ steps.apk-path.outputs.path }}
|
||||||
@@ -70,18 +83,17 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
# fix hardcode changelog file name
|
# fix hardcode changelog file name
|
||||||
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/33300.txt
|
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
name: Release ${{ github.ref_name }}
|
name: ${{ github.ref_name }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: true
|
||||||
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||||
- name: Deploy with fastlane
|
- name: Deploy with fastlane
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.2' # Not needed with a .ruby-version file
|
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
||||||
- name: Distribute app to Beta track 🚀
|
- name: Distribute app to Beta track 🚀
|
||||||
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
||||||
|
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# 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@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: ${{ github.ref_name }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||||
|
- name: Deploy with fastlane
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||||
|
bundler-cache: true
|
||||||
|
- name: Distribute app to Prod track 🚀
|
||||||
|
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane production)
|
||||||
|
|
||||||
@@ -13,16 +13,9 @@ WG Tunnel
|
|||||||
|
|
||||||
|
|
||||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
||||||
[](https://www.amazon.com/gp/product/B0CFGGL7WK)
|
|
||||||
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
[](https://ko-fi.com/N4N8NMJN2)
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -67,11 +60,42 @@ and on while on different networks. This app was created to offer a free solutio
|
|||||||
* Automatic service restart after reboot
|
* Automatic service restart after reboot
|
||||||
* Battery preservation measures
|
* Battery preservation measures
|
||||||
|
|
||||||
|
## Docs (WIP)
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git clone https://github.com/zaneschepke/wgtunnel
|
$ git clone https://github.com/zaneschepke/wgtunnel
|
||||||
$ cd wgtunnel
|
$ cd wgtunnel
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a personal access token (classic) in GitHuv to be able to pull the wireguard-android github dependencies from GitHub packages
|
||||||
|
as documented [here](https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages#authenticating-to-github-packages).
|
||||||
|
|
||||||
|
Alternatively, you can clone [wireguard-android](https://github.com/zaneschepke/wireguard-android) and run the following command to publish the dependency to your local maven repository (requires you have maven installed). This is the ideal approach
|
||||||
|
if you intent to make changes to this lib.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/zaneschepke/wireguard-android
|
||||||
|
$ cd wireguard-android
|
||||||
|
$ brew install maven
|
||||||
|
$ ./gradlew publishToMavenLocal
|
||||||
|
```
|
||||||
|
|
||||||
|
The [wireguard-android](https://github.com/zaneschepke/wireguard-android) dependency is a fork of the official [wireguard-android](https://github.com/WireGuard/wireguard-android) library.
|
||||||
|
|
||||||
|
Add the following lines to local.properties file:
|
||||||
|
```
|
||||||
|
GH_USER=<your github username>
|
||||||
|
GH_TOKEN=<the personal access token with read package permission you just created>
|
||||||
|
```
|
||||||
|
|
||||||
|
And then build the app:
|
||||||
|
```
|
||||||
$ ./gradlew assembleDebug
|
$ ./gradlew assembleDebug
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ plugins {
|
|||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.hilt.android)
|
alias(libs.plugins.hilt.android)
|
||||||
id("org.jetbrains.kotlin.plugin.serialization")
|
alias(libs.plugins.kotlinxSerialization)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,8 +111,7 @@ android {
|
|||||||
create("general") {
|
create("general") {
|
||||||
dimension = Constants.TYPE
|
dimension = Constants.TYPE
|
||||||
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
|
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
|
||||||
apply(plugin = "com.google.gms.google-services")
|
//any plugins general specific
|
||||||
apply(plugin = "com.google.firebase.crashlytics")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,9 +132,12 @@ android {
|
|||||||
val generalImplementation by configurations
|
val generalImplementation by configurations
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
|
implementation(project(":logcatter"))
|
||||||
|
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
// optional - helpers for implementing LifecycleOwner in a Service
|
// helpers for implementing LifecycleOwner in a Service
|
||||||
implementation(libs.androidx.lifecycle.service)
|
implementation(libs.androidx.lifecycle.service)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
@@ -156,13 +158,14 @@ dependencies {
|
|||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
debugImplementation(libs.androidx.compose.manifest)
|
debugImplementation(libs.androidx.compose.manifest)
|
||||||
|
|
||||||
// wg
|
// get tunnel lib from github packages or mavenLocal
|
||||||
implementation(libs.tunnel)
|
implementation(libs.tunnel)
|
||||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
|
|
||||||
// logging
|
// logging
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
|
|
||||||
|
|
||||||
// compose navigation
|
// compose navigation
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
@@ -172,7 +175,6 @@ dependencies {
|
|||||||
ksp(libs.hilt.android.compiler)
|
ksp(libs.hilt.android.compiler)
|
||||||
|
|
||||||
// accompanist
|
// accompanist
|
||||||
implementation(libs.accompanist.systemuicontroller)
|
|
||||||
implementation(libs.accompanist.permissions)
|
implementation(libs.accompanist.permissions)
|
||||||
implementation(libs.accompanist.flowlayout)
|
implementation(libs.accompanist.flowlayout)
|
||||||
implementation(libs.accompanist.drawablepainter)
|
implementation(libs.accompanist.drawablepainter)
|
||||||
@@ -193,17 +195,13 @@ dependencies {
|
|||||||
// serialization
|
// serialization
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
// firebase crashlytics
|
|
||||||
generalImplementation(platform(libs.firebase.bom))
|
|
||||||
generalImplementation(libs.google.firebase.crashlytics.ktx)
|
|
||||||
generalImplementation(libs.google.firebase.analytics.ktx)
|
|
||||||
|
|
||||||
// barcode scanning
|
// barcode scanning
|
||||||
implementation(libs.zxing.android.embedded)
|
implementation(libs.zxing.android.embedded)
|
||||||
implementation(libs.zxing.core)
|
implementation(libs.zxing.core)
|
||||||
|
|
||||||
// bio
|
// bio
|
||||||
implementation(libs.androidx.biometric.ktx)
|
implementation(libs.androidx.biometric.ktx)
|
||||||
|
implementation(libs.pin.lock.compose)
|
||||||
|
|
||||||
// shortcuts
|
// shortcuts
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
{
|
||||||
|
"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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import androidx.room.testing.MigrationTestHelper
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.Queries
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@@ -26,33 +27,7 @@ class MigrationTest {
|
|||||||
helper.createDatabase(dbName, 4).apply {
|
helper.createDatabase(dbName, 4).apply {
|
||||||
// Database has schema version 1. Insert some data using SQL queries.
|
// Database has schema version 1. Insert some data using SQL queries.
|
||||||
// You can't use DAO classes because they expect the latest schema.
|
// You can't use DAO classes because they expect the latest schema.
|
||||||
execSQL(
|
execSQL(Queries.createDefaultSettings())
|
||||||
"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(
|
execSQL(
|
||||||
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
|
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
@@ -23,7 +22,6 @@
|
|||||||
<!--foreground service exempt android 14-->
|
<!--foreground service exempt android 14-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
|
||||||
|
|
||||||
<!--foreground service permissions-->
|
<!--foreground service permissions-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
@@ -84,7 +82,8 @@
|
|||||||
android:screenOrientation="fullSensor"
|
android:screenOrientation="fullSensor"
|
||||||
android:stateNotNeeded="true"
|
android:stateNotNeeded="true"
|
||||||
android:theme="@style/zxing_CaptureTheme"
|
android:theme="@style/zxing_CaptureTheme"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
android:windowSoftInputMode="stateAlwaysHidden"
|
||||||
|
tools:ignore="DiscouragedApi" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".service.shortcut.ShortcutsActivity"
|
android:name=".service.shortcut.ShortcutsActivity"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
@@ -96,12 +95,12 @@
|
|||||||
android:name=".service.foreground.ForegroundService"
|
android:name=".service.foreground.ForegroundService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="systemExempted|specialUse"
|
android:foregroundServiceType="systemExempted"
|
||||||
tools:node="merge" />
|
tools:node="merge" />
|
||||||
<service
|
<service
|
||||||
android:name=".service.tile.TunnelControlTile"
|
android:name=".service.tile.TunnelControlTile"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@drawable/shield"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="WG Tunnel"
|
android:label="WG Tunnel"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -119,7 +118,7 @@
|
|||||||
android:name=".service.foreground.WireGuardTunnelService"
|
android:name=".service.foreground.WireGuardTunnelService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="systemExempted|specialUse"
|
android:foregroundServiceType="systemExempted"
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
android:persistent="true"
|
android:persistent="true"
|
||||||
tools:node="merge">
|
tools:node="merge">
|
||||||
@@ -134,7 +133,7 @@
|
|||||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="systemExempted|specialUse"
|
android:foregroundServiceType="systemExempted"
|
||||||
android:persistent="true"
|
android:persistent="true"
|
||||||
android:stopWithTask="false"
|
android:stopWithTask="false"
|
||||||
tools:node="merge" />
|
tools:node="merge" />
|
||||||
@@ -145,7 +144,6 @@
|
|||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 16 KiB |
@@ -2,20 +2,23 @@ package com.zaneschepke.wireguardautotunnel
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class WireGuardAutoTunnel : Application() {
|
class WireGuardAutoTunnel : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
instance = this
|
instance = this
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
||||||
|
PinManager.initialize(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: WireGuardAutoTunnel
|
lateinit var instance: WireGuardAutoTunnel
|
||||||
private set
|
private set
|
||||||
@@ -24,9 +27,9 @@ class WireGuardAutoTunnel : Application() {
|
|||||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestTileServiceStateUpdate() {
|
fun requestTileServiceStateUpdate(context : Context) {
|
||||||
TileService.requestListeningState(
|
TileService.requestListeningState(
|
||||||
instance,
|
context,
|
||||||
ComponentName(instance, TunnelControlTile::class.java),
|
ComponentName(instance, TunnelControlTile::class.java),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Settings::class, TunnelConfig::class],
|
entities = [Settings::class, TunnelConfig::class],
|
||||||
version = 5,
|
version = 6,
|
||||||
autoMigrations =
|
autoMigrations =
|
||||||
[
|
[
|
||||||
AutoMigration(from = 1, to = 2),
|
AutoMigration(from = 1, to = 2),
|
||||||
@@ -22,6 +22,10 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
|||||||
from = 4,
|
from = 4,
|
||||||
to = 5,
|
to = 5,
|
||||||
),
|
),
|
||||||
|
AutoMigration(
|
||||||
|
from = 5,
|
||||||
|
to = 6,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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,
|
||||||
|
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',
|
||||||
|
'sampleSSID1,sampleSSID2',
|
||||||
|
NULL,
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false',
|
||||||
|
'false')
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import androidx.datastore.preferences.preferencesDataStore
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class DataStoreManager(private val context: Context) {
|
class DataStoreManager(private val context: Context) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -32,7 +33,11 @@ class DataStoreManager(private val context: Context) {
|
|||||||
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
fun <T> getFromStoreFlow(key: Preferences.Key<T>) = context.dataStore.data.map { it[key] }
|
||||||
|
|
||||||
suspend fun <T> getFromStore(key: Preferences.Key<T>) =
|
suspend fun <T> getFromStore(key: Preferences.Key<T>) =
|
||||||
context.dataStore.data.first { it.contains(key) }[key]
|
context.dataStore.data.map{ it[key] }.first()
|
||||||
|
|
||||||
|
fun <T> getFromStoreBlocking(key: Preferences.Key<T>) = runBlocking {
|
||||||
|
context.dataStore.data.map{ it[key] }.first()
|
||||||
|
}
|
||||||
|
|
||||||
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
|
val preferencesFlow: Flow<Preferences?> = context.dataStore.data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ data class Settings(
|
|||||||
defaultValue = "false",
|
defaultValue = "false",
|
||||||
)
|
)
|
||||||
var isAutoTunnelPaused: Boolean = false,
|
var isAutoTunnelPaused: Boolean = false,
|
||||||
|
@ColumnInfo(
|
||||||
|
name = "is_ping_enabled",
|
||||||
|
defaultValue = "false",
|
||||||
|
)
|
||||||
|
var isPingEnabled: Boolean = false,
|
||||||
) {
|
) {
|
||||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
||||||
return if (defaultTunnel != null) {
|
return if (defaultTunnel != null) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -23,6 +24,7 @@ class DatabaseModule {
|
|||||||
context.getString(R.string.db_name),
|
context.getString(R.string.db_name),
|
||||||
)
|
)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
|
.addCallback(DatabaseCallback())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
import com.zaneschepke.wireguardautotunnel.util.goAsync
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -14,10 +16,17 @@ class BootReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
@Inject lateinit var settingsRepository: SettingsRepository
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
|
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
override fun onReceive(context: Context?, intent: Intent?) = goAsync {
|
||||||
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
|
||||||
if (settingsRepository.getSettings().isAutoTunnelEnabled) {
|
val settings = settingsRepository.getSettings()
|
||||||
|
if (settings.isAutoTunnelEnabled) {
|
||||||
|
Timber.i("Starting watcher service from boot")
|
||||||
ServiceManager.startWatcherServiceForeground(context!!)
|
ServiceManager.startWatcherServiceForeground(context!!)
|
||||||
|
} else if(settings.isAlwaysOnVpnEnabled) {
|
||||||
|
Timber.i("Starting tunnel from boot")
|
||||||
|
ServiceManager.startVpnServicePrimaryTunnel(context!!, settings, tunnelConfigRepository.getAll().firstOrNull())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ open class ForegroundService : LifecycleService() {
|
|||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.d("Service stopped without being started: ${e.message}")
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
isServiceStarted = false
|
isServiceStarted = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import android.app.Service
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object ServiceManager {
|
object ServiceManager {
|
||||||
@@ -77,6 +79,15 @@ object ServiceManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startVpnServicePrimaryTunnel(context: Context, settings: Settings, fallbackTunnel: TunnelConfig? = null) {
|
||||||
|
if(settings.defaultTunnel != null) {
|
||||||
|
return startVpnServiceForeground(context, settings.defaultTunnel!!)
|
||||||
|
}
|
||||||
|
if(fallbackTunnel != null) {
|
||||||
|
startVpnServiceForeground(context, fallbackTunnel.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun startWatcherServiceForeground(
|
fun startWatcherServiceForeground(
|
||||||
context: Context,
|
context: Context,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||||
|
|
||||||
import android.app.AlarmManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.os.SystemClock
|
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
@@ -28,10 +24,11 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.net.InetAddress
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||||
private val foregroundId = 122
|
private val foregroundId = 122
|
||||||
@@ -122,45 +119,18 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO could this be restarting service in a bad state?
|
private fun initWakeLock() {
|
||||||
// try to start task again if killed
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent) {
|
|
||||||
Timber.d("Task Removed called")
|
|
||||||
val restartServiceIntent = Intent(rootIntent)
|
|
||||||
val restartServicePendingIntent: PendingIntent =
|
|
||||||
PendingIntent.getService(
|
|
||||||
this,
|
|
||||||
1,
|
|
||||||
restartServiceIntent,
|
|
||||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
|
|
||||||
)
|
|
||||||
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
|
||||||
val alarmService: AlarmManager =
|
|
||||||
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
|
||||||
alarmService.set(
|
|
||||||
AlarmManager.ELAPSED_REALTIME,
|
|
||||||
SystemClock.elapsedRealtime() + 1000,
|
|
||||||
restartServicePendingIntent,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun initWakeLock() {
|
|
||||||
val isBatterySaverOn =
|
|
||||||
withContext(lifecycleScope.coroutineContext) {
|
|
||||||
settingsRepository.getSettings().isBatterySaverEnabled
|
|
||||||
}
|
|
||||||
wakeLock =
|
wakeLock =
|
||||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||||
if (isBatterySaverOn) {
|
try {
|
||||||
Timber.d("Initiating wakelock with timeout")
|
Timber.i("Initiating wakelock with 10 min timeout")
|
||||||
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||||
} else {
|
} finally {
|
||||||
Timber.d("Initiating wakelock with zero timeout")
|
release()
|
||||||
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelWatcherJob() {
|
private fun cancelWatcherJob() {
|
||||||
@@ -174,33 +144,40 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val setting = settingsRepository.getSettings()
|
val setting = settingsRepository.getSettings()
|
||||||
launch {
|
launch {
|
||||||
Timber.d("Starting wifi watcher")
|
Timber.i("Starting wifi watcher")
|
||||||
watchForWifiConnectivityChanges()
|
watchForWifiConnectivityChanges()
|
||||||
}
|
}
|
||||||
if (setting.isTunnelOnMobileDataEnabled) {
|
if (setting.isTunnelOnMobileDataEnabled) {
|
||||||
launch {
|
launch {
|
||||||
Timber.d("Starting mobile data watcher")
|
Timber.i("Starting mobile data watcher")
|
||||||
watchForMobileDataConnectivityChanges()
|
watchForMobileDataConnectivityChanges()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (setting.isTunnelOnEthernetEnabled) {
|
if (setting.isTunnelOnEthernetEnabled) {
|
||||||
launch {
|
launch {
|
||||||
Timber.d("Starting ethernet data watcher")
|
Timber.i("Starting ethernet data watcher")
|
||||||
watchForEthernetConnectivityChanges()
|
watchForEthernetConnectivityChanges()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
Timber.d("Starting vpn state watcher")
|
Timber.i("Starting vpn state watcher")
|
||||||
watchForVpnConnectivityChanges()
|
watchForVpnConnectivityChanges()
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
Timber.d("Starting settings watcher")
|
Timber.i("Starting settings watcher")
|
||||||
watchForSettingsChanges()
|
watchForSettingsChanges()
|
||||||
}
|
}
|
||||||
|
if(setting.isPingEnabled) {
|
||||||
|
launch {
|
||||||
|
Timber.i("Starting ping watcher")
|
||||||
|
watchForPingFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
launch {
|
launch {
|
||||||
Timber.d("Starting management watcher")
|
Timber.i("Starting management watcher")
|
||||||
manageVpn()
|
manageVpn()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +185,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
mobileDataService.networkStatus.collect {
|
mobileDataService.networkStatus.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.d("Gained Mobile data connection")
|
Timber.i("Gained Mobile data connection")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isMobileDataConnected = true,
|
isMobileDataConnected = true,
|
||||||
@@ -219,19 +196,53 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isMobileDataConnected = true,
|
isMobileDataConnected = true,
|
||||||
)
|
)
|
||||||
Timber.d("Mobile data capabilities changed")
|
Timber.i("Mobile data capabilities changed")
|
||||||
}
|
}
|
||||||
is NetworkStatus.Unavailable -> {
|
is NetworkStatus.Unavailable -> {
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isMobileDataConnected = false,
|
isMobileDataConnected = false,
|
||||||
)
|
)
|
||||||
Timber.d("Lost mobile data connection")
|
Timber.i("Lost mobile data connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun watchForPingFailure() {
|
||||||
|
try {
|
||||||
|
do {
|
||||||
|
if(vpnService.vpnState.value.status == Tunnel.State.UP) {
|
||||||
|
val config = vpnService.vpnState.value.config
|
||||||
|
config?.let {
|
||||||
|
val results = it.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)
|
||||||
|
val tunnel = networkEventsFlow.value.settings.defaultTunnel
|
||||||
|
ServiceManager.startVpnServiceForeground(this, tunnel!!)
|
||||||
|
delay(Constants.PING_COOLDOWN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(Constants.PING_INTERVAL)
|
||||||
|
} while (true)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun watchForSettingsChanges() {
|
private suspend fun watchForSettingsChanges() {
|
||||||
settingsRepository.getSettingsFlow().collect {
|
settingsRepository.getSettingsFlow().collect {
|
||||||
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||||
@@ -269,14 +280,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
ethernetService.networkStatus.collect {
|
ethernetService.networkStatus.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.d("Gained Ethernet connection")
|
Timber.i("Gained Ethernet connection")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isEthernetConnected = true,
|
isEthernetConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.d("Ethernet capabilities changed")
|
Timber.i("Ethernet capabilities changed")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isEthernetConnected = true,
|
isEthernetConnected = true,
|
||||||
@@ -287,7 +298,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isEthernetConnected = false,
|
isEthernetConnected = false,
|
||||||
)
|
)
|
||||||
Timber.d("Lost Ethernet connection")
|
Timber.i("Lost Ethernet connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,20 +308,20 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
wifiService.networkStatus.collect {
|
wifiService.networkStatus.collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is NetworkStatus.Available -> {
|
is NetworkStatus.Available -> {
|
||||||
Timber.d("Gained Wi-Fi connection")
|
Timber.i("Gained Wi-Fi connection")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isWifiConnected = true,
|
isWifiConnected = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is NetworkStatus.CapabilitiesChanged -> {
|
is NetworkStatus.CapabilitiesChanged -> {
|
||||||
Timber.d("Wifi capabilities changed")
|
Timber.i("Wifi capabilities changed")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isWifiConnected = true,
|
isWifiConnected = true,
|
||||||
)
|
)
|
||||||
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||||
Timber.d("Detected SSID: $ssid")
|
Timber.i("Detected SSID: $ssid")
|
||||||
networkEventsFlow.value =
|
networkEventsFlow.value =
|
||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
currentNetworkSSID = ssid,
|
currentNetworkSSID = ssid,
|
||||||
@@ -321,16 +332,15 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
networkEventsFlow.value.copy(
|
networkEventsFlow.value.copy(
|
||||||
isWifiConnected = false,
|
isWifiConnected = false,
|
||||||
)
|
)
|
||||||
Timber.d("Lost Wi-Fi connection")
|
Timber.i("Lost Wi-Fi connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO clean this up
|
|
||||||
private suspend fun manageVpn() {
|
private suspend fun manageVpn() {
|
||||||
networkEventsFlow.collectLatest {
|
networkEventsFlow.collectLatest {
|
||||||
Timber.i("New watcher state: $it")
|
val autoTunnel = "Auto-tunnel watcher"
|
||||||
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
|
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
|
||||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||||
when {
|
when {
|
||||||
@@ -338,7 +348,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
it.settings.isTunnelOnEthernetEnabled &&
|
it.settings.isTunnelOnEthernetEnabled &&
|
||||||
!it.isVpnConnected)) -> {
|
!it.isVpnConnected)) -> {
|
||||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
Timber.i("Condition 1 met")
|
Timber.i("$autoTunnel condition 1 met")
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
(!it.isEthernetConnected &&
|
||||||
it.settings.isTunnelOnMobileDataEnabled &&
|
it.settings.isTunnelOnMobileDataEnabled &&
|
||||||
@@ -346,14 +356,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
it.isMobileDataConnected &&
|
it.isMobileDataConnected &&
|
||||||
!it.isVpnConnected) -> {
|
!it.isVpnConnected) -> {
|
||||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
Timber.i("Condition 2 met")
|
Timber.i("$autoTunnel condition 2 met")
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
(!it.isEthernetConnected &&
|
||||||
!it.settings.isTunnelOnMobileDataEnabled &&
|
!it.settings.isTunnelOnMobileDataEnabled &&
|
||||||
!it.isWifiConnected &&
|
!it.isWifiConnected &&
|
||||||
it.isVpnConnected) -> {
|
it.isVpnConnected) -> {
|
||||||
ServiceManager.stopVpnService(this)
|
ServiceManager.stopVpnService(this)
|
||||||
Timber.i("Condition 3 met")
|
Timber.i("$autoTunnel condition 3 met")
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
(!it.isEthernetConnected &&
|
||||||
it.isWifiConnected &&
|
it.isWifiConnected &&
|
||||||
@@ -361,31 +371,31 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
it.settings.isTunnelOnWifiEnabled &&
|
it.settings.isTunnelOnWifiEnabled &&
|
||||||
(!it.isVpnConnected)) -> {
|
(!it.isVpnConnected)) -> {
|
||||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||||
Timber.i("Condition 4 met")
|
Timber.i("$autoTunnel condition 4 met")
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
(!it.isEthernetConnected &&
|
||||||
(it.isWifiConnected &&
|
(it.isWifiConnected &&
|
||||||
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
||||||
(it.isVpnConnected)) -> {
|
(it.isVpnConnected)) -> {
|
||||||
ServiceManager.stopVpnService(this)
|
ServiceManager.stopVpnService(this)
|
||||||
Timber.i("Condition 5 met")
|
Timber.i("$autoTunnel condition 5 met")
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
(!it.isEthernetConnected &&
|
||||||
(it.isWifiConnected &&
|
(it.isWifiConnected &&
|
||||||
!it.settings.isTunnelOnWifiEnabled &&
|
!it.settings.isTunnelOnWifiEnabled &&
|
||||||
(it.isVpnConnected))) -> {
|
(it.isVpnConnected))) -> {
|
||||||
ServiceManager.stopVpnService(this)
|
ServiceManager.stopVpnService(this)
|
||||||
Timber.i("Condition 6 met")
|
Timber.i("$autoTunnel condition 6 met")
|
||||||
}
|
}
|
||||||
(!it.isEthernetConnected &&
|
(!it.isEthernetConnected &&
|
||||||
!it.isWifiConnected &&
|
!it.isWifiConnected &&
|
||||||
!it.isMobileDataConnected &&
|
!it.isMobileDataConnected &&
|
||||||
(it.isVpnConnected)) -> {
|
(it.isVpnConnected)) -> {
|
||||||
ServiceManager.stopVpnService(this)
|
ServiceManager.stopVpnService(this)
|
||||||
Timber.i("Condition 7 met")
|
Timber.i("$autoTunnel condition 7 met")
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Timber.i("No condition met")
|
Timber.i("$autoTunnel no condition met")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
stopService(extras)
|
stopService(extras)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Tunnel config null, starting default tunnel or first")
|
Timber.i("Tunnel config null, starting default tunnel or first")
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
val tunnels = tunnelConfigRepository.getAll()
|
val tunnels = tunnelConfigRepository.getAll()
|
||||||
if (settings.isAlwaysOnVpnEnabled) {
|
if (settings.isAlwaysOnVpnEnabled) {
|
||||||
@@ -84,6 +84,8 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
tunnelName = tunnel.name
|
tunnelName = tunnel.name
|
||||||
vpnService.startTunnel(tunnel)
|
vpnService.startTunnel(tunnel)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
launchAlwaysOnDisabledNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,6 +118,11 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun launchAlwaysOnDisabledNotification() {
|
||||||
|
launchVpnNotification(title = this.getString(R.string.vpn_connection_failed),
|
||||||
|
description = this.getString(R.string.always_on_disabled))
|
||||||
|
}
|
||||||
|
|
||||||
override fun stopService(extras: Bundle?) {
|
override fun stopService(extras: Bundle?) {
|
||||||
super.stopService(extras)
|
super.stopService(extras)
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
|
|||||||
.setOngoing(onGoing)
|
.setOngoing(onGoing)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setShowWhen(showTimestamp)
|
.setShowWhen(showTimestamp)
|
||||||
.setSmallIcon(R.mipmap.ic_launcher_foreground)
|
.setSmallIcon(R.drawable.ic_launcher)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ class TunnelControlTile() : TileService() {
|
|||||||
setUnavailable()
|
setUnavailable()
|
||||||
return@collect
|
return@collect
|
||||||
}
|
}
|
||||||
tunnelName =
|
tunnelName = it.name.run {
|
||||||
it.name.ifBlank {
|
|
||||||
val settings = settingsRepository.getSettings()
|
val settings = settingsRepository.getSettings()
|
||||||
if (settings.defaultTunnel != null) {
|
if (settings.defaultTunnel != null) {
|
||||||
TunnelConfig.from(settings.defaultTunnel!!).name
|
TunnelConfig.from(settings.defaultTunnel!!).name
|
||||||
@@ -72,15 +71,18 @@ class TunnelControlTile() : TileService() {
|
|||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val tunnelConfig =
|
val defaultTunnel = settingsRepository.getSettings().defaultTunnel
|
||||||
tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
val config = defaultTunnel ?: run {
|
||||||
|
val tunnelConfigs = tunnelConfigRepository.getAll()
|
||||||
|
return@run tunnelConfigs.find { it.name == tunnelName }
|
||||||
|
}
|
||||||
toggleWatcherServicePause()
|
toggleWatcherServicePause()
|
||||||
if (vpnService.getState() == Tunnel.State.UP) {
|
if (vpnService.getState() == Tunnel.State.UP) {
|
||||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||||
} else {
|
} else {
|
||||||
ServiceManager.startVpnServiceForeground(
|
ServiceManager.startVpnServiceForeground(
|
||||||
this@TunnelControlTile,
|
this@TunnelControlTile,
|
||||||
tunnelConfig.toString(),
|
config.toString(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
|||||||
|
|
||||||
import com.wireguard.android.backend.Statistics
|
import com.wireguard.android.backend.Statistics
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.wireguard.config.Config
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
|
|
||||||
data class VpnState(
|
data class VpnState(
|
||||||
val status: Tunnel.State = Tunnel.State.DOWN,
|
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||||
val name: String = "",
|
val name: String = "",
|
||||||
|
val config: Config? = null,
|
||||||
val statistics: Statistics? = null
|
val statistics: Statistics? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import javax.inject.Inject
|
|||||||
class WireGuardTunnel
|
class WireGuardTunnel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@Userspace private val userspaceBackend: Backend,
|
@Userspace private val userspaceBackend : Backend,
|
||||||
@Kernel private val kernelBackend: Backend,
|
@Kernel private val kernelBackend: Backend,
|
||||||
private val settingsRepository: SettingsRepository
|
private val settingsRepository: SettingsRepository
|
||||||
) : VpnService {
|
) : VpnService {
|
||||||
@@ -63,6 +63,7 @@ constructor(
|
|||||||
stopTunnelOnConfigChange(tunnelConfig)
|
stopTunnelOnConfigChange(tunnelConfig)
|
||||||
emitTunnelName(tunnelConfig.name)
|
emitTunnelName(tunnelConfig.name)
|
||||||
config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||||
|
emitTunnelConfig(config)
|
||||||
val state =
|
val state =
|
||||||
backend.setState(
|
backend.setState(
|
||||||
this,
|
this,
|
||||||
@@ -71,7 +72,7 @@ constructor(
|
|||||||
)
|
)
|
||||||
emitTunnelState(state)
|
emitTunnelState(state)
|
||||||
state
|
state
|
||||||
} catch (e: Exception) {
|
} catch (e: BackendException) {
|
||||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||||
State.DOWN
|
State.DOWN
|
||||||
}
|
}
|
||||||
@@ -101,6 +102,14 @@ constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun emitTunnelConfig(config : Config?) {
|
||||||
|
_vpnState.emit(
|
||||||
|
_vpnState.value.copy(
|
||||||
|
config = config,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
private suspend fun stopTunnelOnConfigChange(tunnelConfig: TunnelConfig) {
|
||||||
if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
|
if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
|
||||||
stopTunnel()
|
stopTunnel()
|
||||||
@@ -129,7 +138,7 @@ constructor(
|
|||||||
override fun onStateChange(state: State) {
|
override fun onStateChange(state: State) {
|
||||||
val tunnel = this
|
val tunnel = this
|
||||||
emitTunnelState(state)
|
emitTunnelState(state)
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
WireGuardAutoTunnel.requestTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
||||||
if (state == State.UP) {
|
if (state == State.UP) {
|
||||||
statsJob =
|
statsJob =
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
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() {}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
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.data.datastore.DataStoreManager
|
||||||
|
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 kotlinx.coroutines.withContext
|
||||||
|
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,16 +1,17 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui
|
package com.zaneschepke.wireguardautotunnel.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import androidx.activity.SystemBarStyle
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarData
|
import androidx.compose.material3.SnackbarData
|
||||||
@@ -18,44 +19,50 @@ import androidx.compose.material3.SnackbarDuration
|
|||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.wireguard.android.backend.GoBackend
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.TransparentSystemBars
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.logs.LogsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import xyz.teamgravity.pin_lock_compose.PinLock
|
||||||
import java.io.IOException
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -64,81 +71,92 @@ class MainActivity : AppCompatActivity() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var dataStoreManager: DataStoreManager
|
lateinit var dataStoreManager: DataStoreManager
|
||||||
|
|
||||||
@Inject lateinit var settingsRepository: SettingsRepository
|
@Inject
|
||||||
|
lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
)
|
)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
|
||||||
|
|
||||||
// load preferences into memory and init data
|
// load preferences into memory and init data
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
dataStoreManager.init()
|
||||||
dataStoreManager.init()
|
WireGuardAutoTunnel.requestTileServiceStateUpdate(this@MainActivity)
|
||||||
if (settingsRepository.getAll().isEmpty()) {
|
val settings = settingsRepository.getSettings()
|
||||||
settingsRepository.save(com.zaneschepke.wireguardautotunnel.data.model.Settings())
|
if (settings.isAutoTunnelEnabled) {
|
||||||
}
|
ServiceManager.startWatcherService(application.applicationContext)
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e("Failed to load preferences")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setContent {
|
setContent {
|
||||||
// val activityViewModel = hiltViewModel<ActivityViewModel>()
|
val appViewModel = hiltViewModel<AppViewModel>()
|
||||||
|
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
|
||||||
val navController = rememberNavController()
|
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 focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
val vpnActivityResultState =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult(),
|
||||||
|
onResult = {
|
||||||
|
val accepted = (it.resultCode == RESULT_OK)
|
||||||
|
if (accepted) {
|
||||||
|
appViewModel.onVpnPermissionAccepted()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fun showSnackBarMessage(message: StringValue) {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
val result =
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = message.asString(this@MainActivity),
|
||||||
|
duration = SnackbarDuration.Short,
|
||||||
|
)
|
||||||
|
when (result) {
|
||||||
|
SnackbarResult.ActionPerformed,
|
||||||
|
SnackbarResult.Dismissed -> {
|
||||||
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(appUiState.requestPermissions) {
|
||||||
|
if (appUiState.requestPermissions) {
|
||||||
|
appViewModel.permissionsRequested()
|
||||||
|
if (notificationPermissionState != null && !notificationPermissionState.status.isGranted
|
||||||
|
) {
|
||||||
|
showSnackBarMessage(StringValue.StringResource(R.string.notification_permission_required))
|
||||||
|
return@LaunchedEffect notificationPermissionState.launchPermissionRequest()
|
||||||
|
}
|
||||||
|
if (!appUiState.vpnPermissionAccepted) {
|
||||||
|
return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WireguardAutoTunnelTheme {
|
WireguardAutoTunnelTheme {
|
||||||
TransparentSystemBars()
|
LaunchedEffect(Unit) {
|
||||||
|
appViewModel.setNotificationPermissionAccepted(
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
notificationPermissionState?.status?.isGranted ?: true,
|
||||||
|
|
||||||
val notificationPermissionState =
|
|
||||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
|
|
||||||
fun requestNotificationPermission() {
|
|
||||||
if (
|
|
||||||
!notificationPermissionState.status.isGranted &&
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
|
||||||
) {
|
|
||||||
notificationPermissionState.launchPermissionRequest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
|
|
||||||
val vpnActivityResultState =
|
|
||||||
rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult(),
|
|
||||||
onResult = {
|
|
||||||
val accepted = (it.resultCode == RESULT_OK)
|
|
||||||
if (accepted) {
|
|
||||||
vpnIntent = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
LaunchedEffect(vpnIntent) {
|
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
|
||||||
if (vpnIntent != null) {
|
|
||||||
vpnActivityResultState.launch(vpnIntent)
|
|
||||||
} else {
|
|
||||||
requestNotificationPermission()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSnackBarMessage(message: String) {
|
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
if (!appUiState.snackbarMessageConsumed) {
|
||||||
val result =
|
showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage))
|
||||||
snackbarHostState.showSnackbar(
|
appViewModel.snackbarMessageConsumed()
|
||||||
message = message,
|
|
||||||
actionLabel = applicationContext.getString(R.string.okay),
|
|
||||||
duration = SnackbarDuration.Short,
|
|
||||||
)
|
|
||||||
when (result) {
|
|
||||||
SnackbarResult.ActionPerformed,
|
|
||||||
SnackbarResult.Dismissed -> {
|
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,65 +167,45 @@ class MainActivity : AppCompatActivity() {
|
|||||||
snackbarData.visuals.message,
|
snackbarData.visuals.message,
|
||||||
isRtl = false,
|
isRtl = false,
|
||||||
containerColor =
|
containerColor =
|
||||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||||
2.dp,
|
2.dp,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.focusable().focusProperties { up = focusRequester },
|
//TODO refactor
|
||||||
bottomBar =
|
modifier = Modifier
|
||||||
if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
.focusable()
|
||||||
{
|
.focusProperties { when(navBackStackEntry?.destination?.route) {
|
||||||
BottomNavBar(
|
Screen.Lock.route -> Unit
|
||||||
navController,
|
else -> up = focusRequester }
|
||||||
listOf(
|
},
|
||||||
Screen.Main.navItem,
|
bottomBar = {
|
||||||
Screen.Settings.navItem,
|
BottomNavBar(
|
||||||
Screen.Support.navItem,
|
navController,
|
||||||
),
|
listOf(
|
||||||
)
|
Screen.Main.navItem,
|
||||||
}
|
Screen.Settings.navItem,
|
||||||
} else {
|
Screen.Support.navItem,
|
||||||
{}
|
),
|
||||||
},
|
)
|
||||||
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
if (vpnIntent != null) {
|
NavHost(
|
||||||
PermissionRequestFailedScreen(
|
navController,
|
||||||
padding = padding,
|
startDestination =
|
||||||
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
|
(if (PinManager.pinExists()) Screen.Lock.route else Screen.Main.route),
|
||||||
message = getString(R.string.vpn_permission_required),
|
modifier =
|
||||||
getString(R.string.retry),
|
Modifier
|
||||||
)
|
.padding(padding)
|
||||||
return@Scaffold
|
.fillMaxSize(),
|
||||||
}
|
) {
|
||||||
if (!notificationPermissionState.status.isGranted) {
|
|
||||||
PermissionRequestFailedScreen(
|
|
||||||
padding = padding,
|
|
||||||
onRequestAgain = {
|
|
||||||
val intentSettings =
|
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
|
||||||
intentSettings.data =
|
|
||||||
Uri.fromParts(
|
|
||||||
Constants.URI_PACKAGE_SCHEME,
|
|
||||||
this.packageName,
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
startActivity(intentSettings)
|
|
||||||
},
|
|
||||||
message = getString(R.string.notification_permission_required),
|
|
||||||
getString(R.string.open_settings),
|
|
||||||
)
|
|
||||||
return@Scaffold
|
|
||||||
}
|
|
||||||
NavHost(navController, startDestination = Screen.Main.route) {
|
|
||||||
composable(
|
composable(
|
||||||
Screen.Main.route,
|
Screen.Main.route,
|
||||||
) {
|
) {
|
||||||
MainScreen(
|
MainScreen(
|
||||||
padding = padding,
|
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
appViewModel = appViewModel,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -215,34 +213,37 @@ class MainActivity : AppCompatActivity() {
|
|||||||
Screen.Settings.route,
|
Screen.Settings.route,
|
||||||
) {
|
) {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
padding = padding,
|
appViewModel = appViewModel,
|
||||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
navController = navController,
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
Screen.Support.route,
|
Screen.Support.route,
|
||||||
) {
|
) {
|
||||||
SupportScreen(
|
SupportScreen(
|
||||||
padding = padding,
|
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
appViewModel = appViewModel,
|
||||||
|
navController = navController,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
composable(Screen.Support.Logs.route) {
|
||||||
|
LogsScreen(appViewModel)
|
||||||
|
}
|
||||||
composable("${Screen.Config.route}/{id}") {
|
composable("${Screen.Config.route}/{id}") {
|
||||||
val id = it.arguments?.getString("id")
|
val id = it.arguments?.getString("id")
|
||||||
if (!id.isNullOrBlank()) {
|
if (!id.isNullOrBlank()) {
|
||||||
// https://dagger.dev/hilt/view-model#assisted-injection
|
|
||||||
ConfigScreen(
|
ConfigScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
id = id,
|
id = id,
|
||||||
showSnackbarMessage = { message ->
|
appViewModel = appViewModel,
|
||||||
showSnackBarMessage(message)
|
|
||||||
},
|
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
composable(Screen.Lock.route) {
|
||||||
|
PinLockScreen(navController = navController, appViewModel = appViewModel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ sealed class Screen(val route: String) {
|
|||||||
route = route,
|
route = route,
|
||||||
icon = Icons.Rounded.QuestionMark,
|
icon = Icons.Rounded.QuestionMark,
|
||||||
)
|
)
|
||||||
|
data object Logs : Screen("support/logs")
|
||||||
}
|
}
|
||||||
|
|
||||||
data object Config : Screen("config")
|
data object Config : Screen("config")
|
||||||
|
data object Lock : Screen("lock")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ fun ClickableIconButton(
|
|||||||
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = stringResource(R.string.delete),
|
contentDescription = icon.name,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -49,7 +49,6 @@ fun RowListItem(
|
|||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth(.60f),
|
|
||||||
) {
|
) {
|
||||||
icon()
|
icon()
|
||||||
Text(text)
|
Text(text)
|
||||||
@@ -65,6 +64,7 @@ fun RowListItem(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
) {
|
) {
|
||||||
|
//TODO change these to string resources
|
||||||
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||||
val peerTx = statistics.peer(it)!!.txBytes
|
val peerTx = statistics.peer(it)!!.txBytes
|
||||||
val peerRx = statistics.peer(it)!!.rxBytes
|
val peerRx = statistics.peer(it)!!.rxBytes
|
||||||
|
|||||||
@@ -6,18 +6,33 @@ import androidx.compose.material3.NavigationBar
|
|||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
|
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
|
||||||
val backStackEntry = navController.currentBackStackEntryAsState()
|
val backStackEntry = navController.currentBackStackEntryAsState()
|
||||||
|
|
||||||
|
var showBottomBar by rememberSaveable { mutableStateOf(true) }
|
||||||
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
|
|
||||||
|
//TODO find a better way to hide nav bar
|
||||||
|
showBottomBar = when (navBackStackEntry?.destination?.route) {
|
||||||
|
Screen.Lock.route -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = if(!showBottomBar) Color.Transparent else MaterialTheme.colorScheme.background,
|
||||||
) {
|
) {
|
||||||
bottomNavItems.forEach { item ->
|
if(showBottomBar) bottomNavItems.forEach { item ->
|
||||||
val selected = item.route == backStackEntry.value?.destination?.route
|
val selected = item.route == backStackEntry.value?.destination?.route
|
||||||
|
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
@@ -33,7 +32,6 @@ fun CustomSnackBar(
|
|||||||
isRtl: Boolean = true,
|
isRtl: Boolean = true,
|
||||||
containerColor: Color = MaterialTheme.colorScheme.surface
|
containerColor: Color = MaterialTheme.colorScheme.surface
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
Snackbar(
|
Snackbar(
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
modifier =
|
modifier =
|
||||||
@@ -51,9 +49,10 @@ fun CustomSnackBar(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
) {
|
) {
|
||||||
|
val icon = Icons.Rounded.Info
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Info,
|
icon,
|
||||||
contentDescription = stringResource(R.string.info),
|
contentDescription = icon.name,
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.padding(end = 10.dp),
|
modifier = Modifier.padding(end = 10.dp),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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,7 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
package com.zaneschepke.wireguardautotunnel.ui.screens.config
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
@@ -29,7 +28,7 @@ import androidx.compose.material.icons.rounded.ContentCopy
|
|||||||
import androidx.compose.material.icons.rounded.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
import androidx.compose.material.icons.rounded.Refresh
|
||||||
import androidx.compose.material.icons.rounded.Save
|
import androidx.compose.material.icons.rounded.Save
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.BasicAlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
@@ -51,7 +50,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
@@ -63,6 +61,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
@@ -73,6 +72,7 @@ import androidx.navigation.NavController
|
|||||||
import com.google.accompanist.drawablepainter.DrawablePainter
|
import com.google.accompanist.drawablepainter.DrawablePainter
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||||
@@ -86,16 +86,14 @@ import kotlinx.coroutines.delay
|
|||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalComposeUiApi::class,
|
|
||||||
ExperimentalMaterial3Api::class,
|
ExperimentalMaterial3Api::class,
|
||||||
ExperimentalFoundationApi::class,
|
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun ConfigScreen(
|
fun ConfigScreen(
|
||||||
viewModel: ConfigViewModel = hiltViewModel(),
|
viewModel: ConfigViewModel = hiltViewModel(),
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
showSnackbarMessage: (String) -> Unit,
|
appViewModel: AppViewModel,
|
||||||
id: String
|
id: String
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -129,13 +127,16 @@ fun ConfigScreen(
|
|||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
val screenPadding = 5.dp
|
val screenPadding = 5.dp
|
||||||
|
|
||||||
val applicationButtonText = {
|
val applicationButtonText = buildAnnotatedString {
|
||||||
"Tunneling apps: " +
|
append(stringResource(id = R.string.tunneling_apps))
|
||||||
|
append(": ")
|
||||||
if (uiState.isAllApplicationsEnabled) {
|
if (uiState.isAllApplicationsEnabled) {
|
||||||
"all"
|
append(stringResource(id = R.string.all))
|
||||||
} else {
|
} else {
|
||||||
"${uiState.checkedPackageNames.size} " +
|
append("${uiState.checkedPackageNames.size} ")
|
||||||
(if (uiState.include) "included" else "excluded")
|
(if (uiState.include) append(stringResource(id = R.string.included)) else append(
|
||||||
|
stringResource(id = R.string.excluded),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,13 +146,13 @@ fun ConfigScreen(
|
|||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
isAuthenticated = true
|
isAuthenticated = true
|
||||||
},
|
},
|
||||||
onError = { error ->
|
onError = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||||
},
|
},
|
||||||
onFailure = {
|
onFailure = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -161,20 +162,24 @@ fun ConfigScreen(
|
|||||||
remember(uiState.packages) {
|
remember(uiState.packages) {
|
||||||
uiState.packages.sortedBy { viewModel.getPackageLabel(it) }
|
uiState.packages.sortedBy { viewModel.getPackageLabel(it) }
|
||||||
}
|
}
|
||||||
AlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
BasicAlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier
|
||||||
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier
|
||||||
|
.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
@@ -187,8 +192,9 @@ fun ConfigScreen(
|
|||||||
if (!uiState.isAllApplicationsEnabled) {
|
if (!uiState.isAllApplicationsEnabled) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier
|
||||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
@@ -219,8 +225,9 @@ fun ConfigScreen(
|
|||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier
|
||||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
@@ -236,7 +243,9 @@ fun ConfigScreen(
|
|||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier.fillMaxSize().padding(5.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(5.dp),
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
|
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
|
||||||
val drawable =
|
val drawable =
|
||||||
@@ -248,9 +257,10 @@ fun ConfigScreen(
|
|||||||
modifier = Modifier.size(50.dp, 50.dp),
|
modifier = Modifier.size(50.dp, 50.dp),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
val icon = Icons.Rounded.Android
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Android,
|
icon,
|
||||||
stringResource(id = R.string.edit),
|
icon.name,
|
||||||
modifier = Modifier.size(50.dp, 50.dp),
|
modifier = Modifier.size(50.dp, 50.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -262,9 +272,9 @@ fun ConfigScreen(
|
|||||||
Checkbox(
|
Checkbox(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
checked =
|
checked =
|
||||||
(uiState.checkedPackageNames.contains(
|
(uiState.checkedPackageNames.contains(
|
||||||
pack.packageName
|
pack.packageName
|
||||||
)),
|
)),
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
if (it) {
|
if (it) {
|
||||||
viewModel.onAddCheckedPackage(pack.packageName)
|
viewModel.onAddCheckedPackage(pack.packageName)
|
||||||
@@ -279,7 +289,9 @@ fun ConfigScreen(
|
|||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = { showApplicationsDialog = false }) {
|
TextButton(onClick = { showApplicationsDialog = false }) {
|
||||||
@@ -299,7 +311,7 @@ fun ConfigScreen(
|
|||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.padding(bottom = 90.dp).onFocusChanged {
|
Modifier.onFocusChanged {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
}
|
}
|
||||||
@@ -308,10 +320,10 @@ fun ConfigScreen(
|
|||||||
viewModel.onSaveAllChanges().let {
|
viewModel.onSaveAllChanges().let {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Result.Success -> {
|
is Result.Success -> {
|
||||||
showSnackbarMessage(it.data.message)
|
appViewModel.showSnackbarMessage(it.data.message)
|
||||||
navController.navigate(Screen.Main.route)
|
navController.navigate(Screen.Main.route)
|
||||||
}
|
}
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -331,7 +343,10 @@ fun ConfigScreen(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize(),
|
Modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.weight(1f, true)
|
||||||
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
@@ -340,16 +355,20 @@ fun ConfigScreen(
|
|||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
Modifier
|
||||||
|
.fillMaxHeight(fillMaxHeight)
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
} else {
|
} else {
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
})
|
})
|
||||||
.padding(top = 50.dp, bottom = 10.dp),
|
.padding(bottom = 10.dp),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.padding(15.dp).focusGroup(),
|
modifier = Modifier
|
||||||
|
.padding(15.dp)
|
||||||
|
.focusGroup(),
|
||||||
) {
|
) {
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
stringResource(R.string.interface_),
|
stringResource(R.string.interface_),
|
||||||
@@ -361,10 +380,14 @@ fun ConfigScreen(
|
|||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.name),
|
label = stringResource(R.string.name),
|
||||||
hint = stringResource(R.string.tunnel_name).lowercase(),
|
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||||
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequester),
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth().clickable { showAuthPrompt = true },
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { showAuthPrompt = true },
|
||||||
value = uiState.interfaceProxy.privateKey,
|
value = uiState.interfaceProxy.privateKey,
|
||||||
visualTransformation =
|
visualTransformation =
|
||||||
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
||||||
@@ -392,7 +415,9 @@ fun ConfigScreen(
|
|||||||
)
|
)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth().focusRequester(FocusRequester.Default),
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(FocusRequester.Default),
|
||||||
value = uiState.interfaceProxy.publicKey,
|
value = uiState.interfaceProxy.publicKey,
|
||||||
enabled = false,
|
enabled = false,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
@@ -425,7 +450,9 @@ fun ConfigScreen(
|
|||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.addresses),
|
label = stringResource(R.string.addresses),
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(3 / 5f)
|
||||||
|
.padding(end = 5.dp),
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.listenPort,
|
value = uiState.interfaceProxy.listenPort,
|
||||||
@@ -443,7 +470,9 @@ fun ConfigScreen(
|
|||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
label = stringResource(R.string.dns_servers),
|
label = stringResource(R.string.dns_servers),
|
||||||
hint = stringResource(R.string.comma_separated_list),
|
hint = stringResource(R.string.comma_separated_list),
|
||||||
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(3 / 5f)
|
||||||
|
.padding(end = 5.dp),
|
||||||
)
|
)
|
||||||
ConfigurationTextBox(
|
ConfigurationTextBox(
|
||||||
value = uiState.interfaceProxy.mtu,
|
value = uiState.interfaceProxy.mtu,
|
||||||
@@ -456,11 +485,13 @@ fun ConfigScreen(
|
|||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = { showApplicationsDialog = true }) {
|
TextButton(onClick = { showApplicationsDialog = true }) {
|
||||||
Text(applicationButtonText())
|
Text(applicationButtonText.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -473,7 +504,9 @@ fun ConfigScreen(
|
|||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
Modifier
|
||||||
|
.fillMaxHeight(fillMaxHeight)
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
} else {
|
} else {
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
Modifier.fillMaxWidth(fillMaxWidth)
|
||||||
})
|
})
|
||||||
@@ -482,19 +515,24 @@ fun ConfigScreen(
|
|||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp),
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 15.dp)
|
||||||
|
.padding(bottom = 10.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 5.dp),
|
||||||
) {
|
) {
|
||||||
SectionTitle(
|
SectionTitle(
|
||||||
stringResource(R.string.peer),
|
stringResource(R.string.peer),
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
)
|
)
|
||||||
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
|
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
|
||||||
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
|
val icon = Icons.Rounded.Delete
|
||||||
|
Icon(icon, icon.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,7 +608,9 @@ fun ConfigScreen(
|
|||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(bottom = 140.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import android.content.pm.PackageManager
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.wireguard.config.BadConfigException
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.wireguard.config.Interface
|
import com.wireguard.config.Interface
|
||||||
|
import com.wireguard.config.ParseException
|
||||||
import com.wireguard.config.Peer
|
import com.wireguard.config.Peer
|
||||||
import com.wireguard.crypto.Key
|
import com.wireguard.crypto.Key
|
||||||
import com.wireguard.crypto.KeyPair
|
import com.wireguard.crypto.KeyPair
|
||||||
@@ -30,6 +32,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -151,7 +154,7 @@ constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (tunnelConfig != null) {
|
if (tunnelConfig != null) {
|
||||||
saveConfig(tunnelConfig).join()
|
saveConfig(tunnelConfig).join()
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
|
||||||
updateSettingsDefaultTunnel(tunnelConfig)
|
updateSettingsDefaultTunnel(tunnelConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,15 +209,19 @@ constructor(
|
|||||||
val peerList = buildPeerListFromProxyPeers()
|
val peerList = buildPeerListFromProxyPeers()
|
||||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||||
val tunnelConfig =
|
val tunnelConfig = when(uiState.value.tunnel) {
|
||||||
_uiState.value.tunnel?.copy(
|
null -> TunnelConfig(name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
|
||||||
|
else -> uiState.value.tunnel!!.copy(
|
||||||
name = _uiState.value.tunnelName,
|
name = _uiState.value.tunnelName,
|
||||||
wgQuick = config.toWgQuickString(),
|
wgQuick = config.toWgQuickString(),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
updateTunnelConfig(tunnelConfig)
|
updateTunnelConfig(tunnelConfig)
|
||||||
Result.Success(Event.Message.ConfigSaved)
|
Result.Success(Event.Message.ConfigSaved)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.Error(Event.Error.Exception(e))
|
Timber.e(e)
|
||||||
|
val message = e.message?.substringAfter(":", missingDelimiterValue = "")
|
||||||
|
Result.Error(Event.Error.ConfigParseError(message ?: ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,10 @@ import androidx.compose.foundation.gestures.ScrollableDefaults
|
|||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.requiredWidth
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -39,25 +36,24 @@ import androidx.compose.material.icons.filled.QrCode
|
|||||||
import androidx.compose.material.icons.rounded.Add
|
import androidx.compose.material.icons.rounded.Add
|
||||||
import androidx.compose.material.icons.rounded.Bolt
|
import androidx.compose.material.icons.rounded.Bolt
|
||||||
import androidx.compose.material.icons.rounded.Circle
|
import androidx.compose.material.icons.rounded.Circle
|
||||||
|
import androidx.compose.material.icons.rounded.CopyAll
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
import androidx.compose.material.icons.rounded.Edit
|
import androidx.compose.material.icons.rounded.Edit
|
||||||
import androidx.compose.material.icons.rounded.Info
|
import androidx.compose.material.icons.rounded.Info
|
||||||
import androidx.compose.material.icons.rounded.Star
|
import androidx.compose.material.icons.rounded.Star
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Divider
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.MaterialTheme.typography
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -77,10 +73,10 @@ import androidx.compose.ui.focus.onFocusChanged
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
@@ -93,6 +89,7 @@ import com.zaneschepke.wireguardautotunnel.R
|
|||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||||
@@ -104,19 +101,18 @@ import com.zaneschepke.wireguardautotunnel.util.Event
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
viewModel: MainViewModel = hiltViewModel(),
|
viewModel: MainViewModel = hiltViewModel(),
|
||||||
padding: PaddingValues,
|
appViewModel: AppViewModel,
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
showSnackbarMessage: (String) -> Unit,
|
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
@@ -132,18 +128,13 @@ fun MainScreen(
|
|||||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(uiState.loading) {
|
LaunchedEffect(Unit) {
|
||||||
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uiState.loading) {
|
|
||||||
LoadingScreen()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val tunnelFileImportResultLauncher =
|
val tunnelFileImportResultLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(
|
||||||
object : ActivityResultContracts.GetContent() {
|
object : ActivityResultContracts.GetContent() {
|
||||||
@@ -173,7 +164,7 @@ fun MainScreen(
|
|||||||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||||
}
|
}
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
@@ -183,7 +174,7 @@ fun MainScreen(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.onTunnelFileSelected(data).let {
|
viewModel.onTunnelFileSelected(data).let {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||||
is Result.Success -> {}
|
is Result.Success -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,7 +189,7 @@ fun MainScreen(
|
|||||||
viewModel.onTunnelQrResult(it.contents).let { result ->
|
viewModel.onTunnelQrResult(it.contents).let { result ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is Result.Success -> {}
|
is Result.Success -> {}
|
||||||
is Result.Error -> showSnackbarMessage(result.error.message)
|
is Result.Error -> appViewModel.showSnackbarMessage(result.error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,63 +246,25 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||||
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
if(appViewModel.isRequiredPermissionGranted()) {
|
||||||
|
if (checked) viewModel.onTunnelStart(tunnel) else viewModel.onTunnelStop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(uiState.loading) {
|
||||||
|
return LoadingScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.pointerInput(Unit) {
|
Modifier.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onTap = {
|
onTap = {
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null
|
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isVisible.value,
|
visible = isVisible.value,
|
||||||
@@ -323,18 +276,17 @@ fun MainScreen(
|
|||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
modifier =
|
modifier =
|
||||||
(if (
|
(if (
|
||||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||||
uiState.tunnels.isEmpty()
|
uiState.tunnels.isEmpty()
|
||||||
)
|
)
|
||||||
Modifier.focusRequester(focusRequester)
|
Modifier.focusRequester(focusRequester)
|
||||||
else Modifier)
|
else Modifier)
|
||||||
.padding(bottom = 90.dp)
|
.onFocusChanged {
|
||||||
.onFocusChanged {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
}
|
||||||
}
|
},
|
||||||
},
|
|
||||||
onClick = { showBottomSheet = true },
|
onClick = { showBottomSheet = true },
|
||||||
containerColor = fobColor,
|
containerColor = fobColor,
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
@@ -347,12 +299,13 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { innerPadding ->
|
) {
|
||||||
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier.fillMaxSize().padding(padding),
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
||||||
}
|
}
|
||||||
@@ -365,12 +318,13 @@ fun MainScreen(
|
|||||||
// Sheet content
|
// Sheet content
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier
|
||||||
.clickable {
|
.fillMaxWidth()
|
||||||
showBottomSheet = false
|
.clickable {
|
||||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
showBottomSheet = false
|
||||||
}
|
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||||
.padding(10.dp),
|
}
|
||||||
|
.padding(10.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.FileOpen,
|
Icons.Filled.FileOpen,
|
||||||
@@ -383,26 +337,27 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier
|
||||||
.clickable {
|
.fillMaxWidth()
|
||||||
scope.launch {
|
.clickable {
|
||||||
showBottomSheet = false
|
scope.launch {
|
||||||
val scanOptions = ScanOptions()
|
showBottomSheet = false
|
||||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
val scanOptions = ScanOptions()
|
||||||
scanOptions.setOrientationLocked(true)
|
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
scanOptions.setPrompt(
|
scanOptions.setOrientationLocked(true)
|
||||||
context.getString(R.string.scanning_qr)
|
scanOptions.setPrompt(
|
||||||
)
|
context.getString(R.string.scanning_qr),
|
||||||
scanOptions.setBeepEnabled(false)
|
)
|
||||||
scanOptions.captureActivity =
|
scanOptions.setBeepEnabled(false)
|
||||||
CaptureActivityPortrait::class.java
|
scanOptions.captureActivity =
|
||||||
scanLauncher.launch(scanOptions)
|
CaptureActivityPortrait::class.java
|
||||||
}
|
scanLauncher.launch(scanOptions)
|
||||||
}
|
}
|
||||||
.padding(10.dp),
|
}
|
||||||
|
.padding(10.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.QrCode,
|
Icons.Filled.QrCode,
|
||||||
@@ -415,17 +370,18 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier
|
||||||
.clickable {
|
.fillMaxWidth()
|
||||||
showBottomSheet = false
|
.clickable {
|
||||||
navController.navigate(
|
showBottomSheet = false
|
||||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
navController.navigate(
|
||||||
)
|
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
||||||
}
|
)
|
||||||
.padding(10.dp),
|
}
|
||||||
|
.padding(10.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Create,
|
Icons.Filled.Create,
|
||||||
@@ -444,15 +400,62 @@ fun MainScreen(
|
|||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier
|
||||||
.fillMaxHeight(.90f)
|
.fillMaxWidth()
|
||||||
.overscroll(ScrollableDefaults.overscrollEffect())
|
.overscroll(ScrollableDefaults.overscrollEffect()),
|
||||||
.padding(innerPadding),
|
|
||||||
state = rememberLazyListState(0, uiState.tunnels.count()),
|
state = rememberLazyListState(0, uiState.tunnels.count()),
|
||||||
userScrollEnabled = true,
|
userScrollEnabled = true,
|
||||||
reverseLayout = true,
|
reverseLayout = false,
|
||||||
flingBehavior = ScrollableDefaults.flingBehavior(),
|
flingBehavior = ScrollableDefaults.flingBehavior(),
|
||||||
) {
|
) {
|
||||||
|
item {
|
||||||
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
|
val autoTunnelingLabel = buildAnnotatedString {
|
||||||
|
append(stringResource(id = R.string.auto_tunneling))
|
||||||
|
append(": ")
|
||||||
|
if (uiState.settings.isAutoTunnelPaused) append(
|
||||||
|
stringResource(id = R.string.paused),
|
||||||
|
) else append(
|
||||||
|
stringResource(id = R.string.active),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
RowListItem(
|
||||||
|
icon = {
|
||||||
|
val icon = Icons.Rounded.Bolt
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
icon.name,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 10.dp)
|
||||||
|
.size(25.dp),
|
||||||
|
tint =
|
||||||
|
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
||||||
|
else mint,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = autoTunnelingLabel.text,
|
||||||
|
rowButton = {
|
||||||
|
if (uiState.settings.isAutoTunnelPaused) {
|
||||||
|
TextButton(
|
||||||
|
onClick = { viewModel.resumeAutoTunneling() },
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.resume))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TextButton(
|
||||||
|
onClick = { viewModel.pauseAutoTunneling() },
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.pause))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {},
|
||||||
|
onHold = {},
|
||||||
|
expanded = false,
|
||||||
|
statistics = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
items(
|
items(
|
||||||
uiState.tunnels,
|
uiState.tunnels,
|
||||||
key = { tunnel -> tunnel.id },
|
key = { tunnel -> tunnel.id },
|
||||||
@@ -460,7 +463,7 @@ fun MainScreen(
|
|||||||
val leadingIconColor =
|
val leadingIconColor =
|
||||||
(if (
|
(if (
|
||||||
uiState.vpnState.name == tunnel.name &&
|
uiState.vpnState.name == tunnel.name &&
|
||||||
uiState.vpnState.status == Tunnel.State.UP
|
uiState.vpnState.status == Tunnel.State.UP
|
||||||
) {
|
) {
|
||||||
uiState.vpnState.statistics
|
uiState.vpnState.statistics
|
||||||
?.mapPeerStats()
|
?.mapPeerStats()
|
||||||
@@ -471,6 +474,7 @@ fun MainScreen(
|
|||||||
statuses?.any { it == HandshakeStatus.STALE } == true -> corn
|
statuses?.any { it == HandshakeStatus.STALE } == true -> corn
|
||||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
|
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
|
||||||
Color.Gray
|
Color.Gray
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Color.Gray
|
Color.Gray
|
||||||
}
|
}
|
||||||
@@ -487,24 +491,28 @@ fun MainScreen(
|
|||||||
Icons.Rounded.Star,
|
Icons.Rounded.Star,
|
||||||
stringResource(R.string.status),
|
stringResource(R.string.status),
|
||||||
tint = leadingIconColor,
|
tint = leadingIconColor,
|
||||||
modifier = Modifier.padding(end = 10.dp).size(20.dp),
|
modifier = Modifier
|
||||||
|
.padding(end = 10.dp)
|
||||||
|
.size(20.dp),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Circle,
|
Icons.Rounded.Circle,
|
||||||
stringResource(R.string.status),
|
stringResource(R.string.status),
|
||||||
tint = leadingIconColor,
|
tint = leadingIconColor,
|
||||||
modifier = Modifier.padding(end = 15.dp).size(15.dp),
|
modifier = Modifier
|
||||||
|
.padding(end = 15.dp)
|
||||||
|
.size(15.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
text = tunnel.name,
|
text = tunnel.name.truncateWithEllipsis(15),
|
||||||
onHold = {
|
onHold = {
|
||||||
if (
|
if (
|
||||||
(uiState.vpnState.status == Tunnel.State.UP) &&
|
(uiState.vpnState.status == Tunnel.State.UP) &&
|
||||||
(tunnel.name == uiState.vpnState.name)
|
(tunnel.name == uiState.vpnState.name)
|
||||||
) {
|
) {
|
||||||
showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message)
|
||||||
return@RowListItem
|
return@RowListItem
|
||||||
}
|
}
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
@@ -514,7 +522,7 @@ fun MainScreen(
|
|||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
(uiState.vpnState.name == tunnel.name)
|
(uiState.vpnState.name == tunnel.name)
|
||||||
) {
|
) {
|
||||||
expanded.value = !expanded.value
|
expanded.value = !expanded.value
|
||||||
}
|
}
|
||||||
@@ -528,7 +536,7 @@ fun MainScreen(
|
|||||||
rowButton = {
|
rowButton = {
|
||||||
if (
|
if (
|
||||||
tunnel.id == selectedTunnel?.id &&
|
tunnel.id == selectedTunnel?.id &&
|
||||||
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||||
@@ -536,9 +544,9 @@ fun MainScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.settings.isAutoTunnelEnabled &&
|
uiState.settings.isAutoTunnelEnabled &&
|
||||||
!uiState.settings.isAutoTunnelPaused
|
!uiState.settings.isAutoTunnelPaused
|
||||||
) {
|
) {
|
||||||
showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
Event.Message.AutoTunnelOffAction.message,
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -546,9 +554,10 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
val icon = Icons.Rounded.Star
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Star,
|
icon,
|
||||||
stringResource(id = R.string.set_primary),
|
icon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -556,12 +565,12 @@ fun MainScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.settings.isAutoTunnelEnabled &&
|
uiState.settings.isAutoTunnelEnabled &&
|
||||||
uiState.settings.isTunnelConfigDefault(
|
uiState.settings.isTunnelConfigDefault(
|
||||||
tunnel,
|
tunnel,
|
||||||
) &&
|
) &&
|
||||||
!uiState.settings.isAutoTunnelPaused
|
!uiState.settings.isAutoTunnelPaused
|
||||||
) {
|
) {
|
||||||
showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
Event.Message.AutoTunnelOffAction.message,
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
)
|
)
|
||||||
} else
|
} else
|
||||||
@@ -570,13 +579,22 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
val icon = Icons.Rounded.Edit
|
||||||
|
Icon(icon, icon.name)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.focusable(),
|
||||||
|
onClick = { viewModel.onCopyTunnel(selectedTunnel) },
|
||||||
|
) {
|
||||||
|
val icon = Icons.Rounded.CopyAll
|
||||||
|
Icon(icon, icon.name)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusable(),
|
modifier = Modifier.focusable(),
|
||||||
onClick = { showDeleteTunnelAlertDialog = true },
|
onClick = { showDeleteTunnelAlertDialog = true },
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
|
val icon = Icons.Rounded.Delete
|
||||||
|
Icon(icon, icon.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -604,7 +622,7 @@ fun MainScreen(
|
|||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.settings.isAutoTunnelEnabled) {
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
Event.Message.AutoTunnelOffAction.message,
|
Event.Message.AutoTunnelOffAction.message,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -613,9 +631,10 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
val icon = Icons.Rounded.Star
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Star,
|
icon,
|
||||||
stringResource(id = R.string.set_primary),
|
icon.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -624,26 +643,27 @@ fun MainScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
(uiState.vpnState.name == tunnel.name)
|
(uiState.vpnState.name == tunnel.name)
|
||||||
) {
|
) {
|
||||||
expanded.value = !expanded.value
|
expanded.value = !expanded.value
|
||||||
} else {
|
} else {
|
||||||
showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
Event.Message.TunnelOnAction.message
|
Event.Message.TunnelOnAction.message,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
val icon = Icons.Rounded.Info
|
||||||
|
Icon(icon, icon.name)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
tunnel.name == uiState.vpnState.name
|
tunnel.name == uiState.vpnState.name
|
||||||
) {
|
) {
|
||||||
showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
Event.Message.TunnelOffAction.message
|
Event.Message.TunnelOffAction.message,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
@@ -652,25 +672,34 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
val icon = Icons.Rounded.Edit
|
||||||
|
Icon(icon, icon.name)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = { viewModel.onCopyTunnel(tunnel) },
|
||||||
|
) {
|
||||||
|
val icon = Icons.Rounded.CopyAll
|
||||||
|
Icon(icon, icon.name)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.vpnState.status == Tunnel.State.UP &&
|
uiState.vpnState.status == Tunnel.State.UP &&
|
||||||
tunnel.name == uiState.vpnState.name
|
tunnel.name == uiState.vpnState.name
|
||||||
) {
|
) {
|
||||||
showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
Event.Message.TunnelOffAction.message
|
Event.Message.TunnelOffAction.message,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
selectedTunnel = tunnel
|
||||||
showDeleteTunnelAlertDialog = true
|
showDeleteTunnelAlertDialog = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
val icon = Icons.Rounded.Delete
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Delete,
|
icon,
|
||||||
stringResource(id = R.string.delete),
|
icon.name
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TunnelSwitch()
|
TunnelSwitch()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.net.Uri
|
|||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.google.zxing.common.StringUtils
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
@@ -49,7 +50,6 @@ constructor(
|
|||||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||||
vpnService.vpnState,
|
vpnService.vpnState,
|
||||||
) { settings, tunnels, vpnState ->
|
) { settings, tunnels, vpnState ->
|
||||||
validateWatcherServiceState(settings)
|
|
||||||
MainUiState(settings, tunnels, vpnState, false)
|
MainUiState(settings, tunnels, vpnState, false)
|
||||||
}
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
@@ -58,13 +58,6 @@ constructor(
|
|||||||
MainUiState(),
|
MainUiState(),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun validateWatcherServiceState(settings: Settings) =
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
if (settings.isAutoTunnelEnabled) {
|
|
||||||
ServiceManager.startWatcherService(application.applicationContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopWatcherService() =
|
private fun stopWatcherService() =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
ServiceManager.stopWatcherService(application.applicationContext)
|
ServiceManager.stopWatcherService(application.applicationContext)
|
||||||
@@ -72,16 +65,17 @@ constructor(
|
|||||||
|
|
||||||
fun onDelete(tunnel: TunnelConfig) {
|
fun onDelete(tunnel: TunnelConfig) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
if (tunnelConfigRepository.count() == 1) {
|
val settings = settingsRepository.getSettings()
|
||||||
|
val isDefault = settings.isTunnelConfigDefault(tunnel)
|
||||||
|
if (tunnelConfigRepository.count() == 1 || isDefault) {
|
||||||
stopWatcherService()
|
stopWatcherService()
|
||||||
val settings = settingsRepository.getSettings()
|
|
||||||
settings.defaultTunnel = null
|
settings.defaultTunnel = null
|
||||||
settings.isAutoTunnelEnabled = false
|
settings.isAutoTunnelEnabled = false
|
||||||
settings.isAlwaysOnVpnEnabled = false
|
settings.isAlwaysOnVpnEnabled = false
|
||||||
saveSettings(settings)
|
saveSettings(settings)
|
||||||
}
|
}
|
||||||
tunnelConfigRepository.delete(tunnel)
|
tunnelConfigRepository.delete(tunnel)
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +100,7 @@ constructor(
|
|||||||
|
|
||||||
fun onTunnelStop() =
|
fun onTunnelStop() =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
Timber.d("Stopping active tunnel")
|
Timber.i("Stopping active tunnel")
|
||||||
ServiceManager.stopVpnService(application.applicationContext)
|
ServiceManager.stopVpnService(application.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +116,7 @@ constructor(
|
|||||||
addTunnel(tunnelConfig)
|
addTunnel(tunnelConfig)
|
||||||
Result.Success(Unit)
|
Result.Success(Unit)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
Result.Error(Event.Error.InvalidQrCode)
|
Result.Error(Event.Error.InvalidQrCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,6 +153,7 @@ constructor(
|
|||||||
return Result.Error(Event.Error.InvalidFileExtension)
|
return Result.Error(Event.Error.InvalidFileExtension)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
return Result.Error(Event.Error.FileReadFailed)
|
return Result.Error(Event.Error.FileReadFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,8 +186,9 @@ constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||||
|
val firstTunnel = tunnelConfigRepository.count() == 0
|
||||||
saveTunnel(tunnelConfig)
|
saveTunnel(tunnelConfig)
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
if(firstTunnel) WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pauseAutoTunneling() =
|
fun pauseAutoTunneling() =
|
||||||
@@ -249,6 +246,7 @@ constructor(
|
|||||||
return try {
|
return try {
|
||||||
fileName.substring(fileName.lastIndexOf('.'))
|
fileName.substring(fileName.lastIndexOf('.'))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,7 +259,13 @@ constructor(
|
|||||||
if (selectedTunnel != null) {
|
if (selectedTunnel != null) {
|
||||||
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
|
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
|
||||||
.join()
|
.join()
|
||||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onCopyTunnel(tunnel: TunnelConfig?) = viewModelScope.launch {
|
||||||
|
tunnel?.let {
|
||||||
|
saveTunnel(TunnelConfig(name = it.name.plus(NumberUtils.randomThree()), wgQuick = it.wgQuick))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -20,9 +20,7 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -55,7 +53,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusProperties
|
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -69,6 +66,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.navigation.NavController
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
@@ -76,16 +74,19 @@ import com.wireguard.android.backend.Tunnel
|
|||||||
import com.wireguard.android.backend.WgQuickBackend
|
import com.wireguard.android.backend.WgQuickBackend
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
@@ -95,15 +96,16 @@ import java.io.File
|
|||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
viewModel: SettingsViewModel = hiltViewModel(),
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
padding: PaddingValues,
|
appViewModel: AppViewModel,
|
||||||
showSnackbarMessage: (String) -> Unit,
|
navController: NavController,
|
||||||
focusRequester: FocusRequester
|
focusRequester: FocusRequester,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
@@ -113,21 +115,14 @@ fun SettingsScreen(
|
|||||||
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||||
var didExportFiles by remember { mutableStateOf(false) }
|
var didExportFiles by remember { mutableStateOf(false) }
|
||||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
val focusRequester2 = remember { FocusRequester() }
|
|
||||||
|
|
||||||
val screenPadding = 5.dp
|
val screenPadding = 5.dp
|
||||||
val fillMaxWidth = .85f
|
val fillMaxWidth = .85f
|
||||||
|
|
||||||
if (uiState.loading) {
|
|
||||||
LoadingScreen()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val startForResult =
|
val startForResult =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||||
result: ActivityResult ->
|
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
val intent = result.data
|
result.data
|
||||||
// Handle the Intent
|
// Handle the Intent
|
||||||
}
|
}
|
||||||
viewModel.setBatteryOptimizeDisableShown()
|
viewModel.setBatteryOptimizeDisableShown()
|
||||||
@@ -141,9 +136,10 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
FileUtils.saveFilesToZip(context, files)
|
FileUtils.saveFilesToZip(context, files)
|
||||||
didExportFiles = true
|
didExportFiles = true
|
||||||
showSnackbarMessage(Event.Message.ConfigsExported.message)
|
appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
Timber.e(e)
|
||||||
|
appViewModel.showSnackbarMessage(Event.Error.Exception(e).message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +159,9 @@ fun SettingsScreen(
|
|||||||
|
|
||||||
fun handleAutoTunnelToggle() {
|
fun handleAutoTunnelToggle() {
|
||||||
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
||||||
viewModel.toggleAutoTunnel()
|
if (appViewModel.isRequiredPermissionGranted()) {
|
||||||
|
viewModel.toggleAutoTunnel()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
requestBatteryOptimizationsDisabled()
|
requestBatteryOptimizationsDisabled()
|
||||||
}
|
}
|
||||||
@@ -174,7 +172,7 @@ fun SettingsScreen(
|
|||||||
viewModel.onSaveTrustedSSID(currentText).let {
|
viewModel.onSaveTrustedSSID(currentText).let {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Result.Success -> currentText = ""
|
is Result.Success -> currentText = ""
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +199,7 @@ fun SettingsScreen(
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
if (
|
if (
|
||||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
||||||
) {
|
) {
|
||||||
checkFineLocationGranted()
|
checkFineLocationGranted()
|
||||||
} else {
|
} else {
|
||||||
@@ -248,12 +246,16 @@ fun SettingsScreen(
|
|||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.LocationOff,
|
Icons.Rounded.LocationOff,
|
||||||
contentDescription = stringResource(id = R.string.map),
|
contentDescription = stringResource(id = R.string.map),
|
||||||
modifier = Modifier.padding(30.dp).size(128.dp),
|
modifier = Modifier
|
||||||
|
.padding(30.dp)
|
||||||
|
.size(128.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.prominent_background_location_title),
|
stringResource(R.string.prominent_background_location_title),
|
||||||
@@ -269,11 +271,15 @@ fun SettingsScreen(
|
|||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier.fillMaxWidth().padding(10.dp)
|
Modifier
|
||||||
} else {
|
.fillMaxWidth()
|
||||||
Modifier.fillMaxWidth().padding(30.dp)
|
.padding(10.dp)
|
||||||
},
|
} else {
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(30.dp)
|
||||||
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
) {
|
) {
|
||||||
@@ -301,11 +307,11 @@ fun SettingsScreen(
|
|||||||
},
|
},
|
||||||
onError = { _ ->
|
onError = { _ ->
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||||
},
|
},
|
||||||
onFailure = {
|
onFailure = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -314,7 +320,7 @@ fun SettingsScreen(
|
|||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier.fillMaxSize().padding(padding),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.one_tunnel_required),
|
stringResource(R.string.one_tunnel_required),
|
||||||
@@ -329,12 +335,15 @@ fun SettingsScreen(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
|
Modifier
|
||||||
indication = null,
|
.fillMaxSize()
|
||||||
interactionSource = interactionSource,
|
.verticalScroll(scrollState)
|
||||||
) {
|
.clickable(
|
||||||
focusManager.clearFocus()
|
indication = null,
|
||||||
},
|
interactionSource = interactionSource,
|
||||||
|
) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
@@ -342,14 +351,17 @@ fun SettingsScreen(
|
|||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier.height(IntrinsicSize.Min)
|
Modifier
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.height(IntrinsicSize.Min)
|
||||||
.padding(top = 10.dp)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
} else {
|
.padding(top = 10.dp)
|
||||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp)
|
} else {
|
||||||
})
|
Modifier
|
||||||
.padding(bottom = 10.dp),
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(top = 20.dp)
|
||||||
|
})
|
||||||
|
.padding(bottom = 10.dp),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
@@ -363,38 +375,43 @@ fun SettingsScreen(
|
|||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(id = R.string.tunnel_on_wifi),
|
stringResource(id = R.string.tunnel_on_wifi),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
checked = uiState.settings.isTunnelOnWifiEnabled,
|
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
||||||
modifier =
|
modifier =
|
||||||
if (uiState.settings.isAutoTunnelEnabled) Modifier
|
if (uiState.settings.isAutoTunnelEnabled) Modifier
|
||||||
else
|
else
|
||||||
Modifier.focusRequester(focusRequester).focusProperties {
|
Modifier
|
||||||
down = focusRequester2
|
.focusRequester(focusRequester),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
||||||
Column {
|
Column {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.padding(screenPadding)
|
||||||
|
.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
) {
|
) {
|
||||||
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
||||||
ClickableIconButton(
|
ClickableIconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
viewModel.onDeleteTrustedSSID(ssid)
|
viewModel.onDeleteTrustedSSID(ssid)
|
||||||
focusRequester2.requestFocus()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
|
onIconClick = {
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
|
||||||
|
viewModel.onDeleteTrustedSSID(ssid)
|
||||||
|
|
||||||
|
},
|
||||||
text = ssid,
|
text = ssid,
|
||||||
icon = Icons.Filled.Close,
|
icon = Icons.Filled.Close,
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
||||||
@@ -407,24 +424,24 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
value = currentText,
|
value = currentText,
|
||||||
onValueChange = { currentText = it },
|
onValueChange = { currentText = it },
|
||||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.padding(
|
Modifier
|
||||||
start = screenPadding,
|
.padding(
|
||||||
top = 5.dp,
|
start = screenPadding,
|
||||||
bottom = 10.dp,
|
top = 5.dp,
|
||||||
)
|
bottom = 10.dp,
|
||||||
.focusRequester(focusRequester2),
|
),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
keyboardOptions =
|
keyboardOptions =
|
||||||
KeyboardOptions(
|
KeyboardOptions(
|
||||||
capitalization = KeyboardCapitalization.None,
|
capitalization = KeyboardCapitalization.None,
|
||||||
imeAction = ImeAction.Done,
|
imeAction = ImeAction.Done,
|
||||||
),
|
),
|
||||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (currentText != "") {
|
if (currentText != "") {
|
||||||
@@ -432,19 +449,19 @@ fun SettingsScreen(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Add,
|
imageVector = Icons.Outlined.Add,
|
||||||
contentDescription =
|
contentDescription =
|
||||||
if (currentText == "") {
|
if (currentText == "") {
|
||||||
stringResource(
|
stringResource(
|
||||||
id =
|
id =
|
||||||
R.string
|
R.string
|
||||||
.trusted_ssid_empty_description,
|
.trusted_ssid_empty_description,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
stringResource(
|
stringResource(
|
||||||
id =
|
id =
|
||||||
R.string
|
R.string
|
||||||
.trusted_ssid_value_description,
|
.trusted_ssid_value_description,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -456,8 +473,8 @@ fun SettingsScreen(
|
|||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.tunnel_mobile_data),
|
stringResource(R.string.tunnel_mobile_data),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
||||||
@@ -465,29 +482,29 @@ fun SettingsScreen(
|
|||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(id = R.string.tunnel_on_ethernet),
|
stringResource(id = R.string.tunnel_on_ethernet),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
|
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
|
||||||
)
|
)
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.battery_saver),
|
stringResource(R.string.restart_on_ping),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled),
|
uiState.settings.isAlwaysOnVpnEnabled),
|
||||||
checked = uiState.settings.isBatterySaverEnabled,
|
checked = uiState.settings.isPingEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleBatterySaver() },
|
onCheckChanged = { viewModel.onToggleRestartOnPing() },
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier =
|
modifier =
|
||||||
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
||||||
else
|
else
|
||||||
Modifier.focusRequester(
|
Modifier.focusRequester(
|
||||||
focusRequester,
|
focusRequester,
|
||||||
))
|
))
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(top = 5.dp),
|
.padding(top = 5.dp),
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
@@ -497,19 +514,22 @@ fun SettingsScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (
|
if (
|
||||||
uiState.settings.isTunnelOnWifiEnabled &&
|
uiState.settings.isTunnelOnWifiEnabled &&
|
||||||
!uiState.settings.isAutoTunnelEnabled
|
!uiState.settings.isAutoTunnelEnabled
|
||||||
) {
|
) {
|
||||||
when (false) {
|
when (false) {
|
||||||
isBackgroundLocationGranted ->
|
isBackgroundLocationGranted ->
|
||||||
showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
Event.Error.BackgroundLocationRequired.message
|
Event.Error.BackgroundLocationRequired.message,
|
||||||
)
|
)
|
||||||
|
|
||||||
fineLocationState.status.isGranted ->
|
fineLocationState.status.isGranted ->
|
||||||
showSnackbarMessage(
|
appViewModel.showSnackbarMessage(
|
||||||
Event.Error.PreciseLocationRequired.message
|
Event.Error.PreciseLocationRequired.message,
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModel.isLocationEnabled(context) ->
|
viewModel.isLocationEnabled(context) ->
|
||||||
showLocationServicesAlertDialog = true
|
showLocationServicesAlertDialog = true
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
handleAutoTunnelToggle()
|
handleAutoTunnelToggle()
|
||||||
}
|
}
|
||||||
@@ -536,7 +556,9 @@ fun SettingsScreen(
|
|||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(vertical = 10.dp),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
@@ -550,15 +572,15 @@ fun SettingsScreen(
|
|||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.use_kernel),
|
stringResource(R.string.use_kernel),
|
||||||
enabled =
|
enabled =
|
||||||
!(uiState.settings.isAutoTunnelEnabled ||
|
!(uiState.settings.isAutoTunnelEnabled ||
|
||||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||||
(uiState.vpnState.status == Tunnel.State.UP)),
|
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||||
checked = uiState.settings.isKernelEnabled,
|
checked = uiState.settings.isKernelEnabled,
|
||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = {
|
onCheckChanged = {
|
||||||
viewModel.onToggleKernelMode().let {
|
viewModel.onToggleKernelMode().let {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||||
is Result.Success -> {}
|
is Result.Success -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,26 +589,27 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
Surface(
|
||||||
Surface(
|
tonalElevation = 2.dp,
|
||||||
tonalElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shape = RoundedCornerShape(12.dp),
|
||||||
shape = RoundedCornerShape(12.dp),
|
color = MaterialTheme.colorScheme.surface,
|
||||||
color = MaterialTheme.colorScheme.surface,
|
modifier =
|
||||||
modifier =
|
Modifier
|
||||||
Modifier.fillMaxWidth(fillMaxWidth)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
.padding(vertical = 10.dp)
|
.padding(vertical = 10.dp)
|
||||||
.padding(bottom = 140.dp),
|
.padding(bottom = 140.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier.padding(15.dp),
|
||||||
) {
|
) {
|
||||||
Column(
|
SectionTitle(
|
||||||
horizontalAlignment = Alignment.Start,
|
title = stringResource(id = R.string.other),
|
||||||
verticalArrangement = Arrangement.Top,
|
padding = screenPadding,
|
||||||
modifier = Modifier.padding(15.dp),
|
)
|
||||||
) {
|
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
SectionTitle(
|
|
||||||
title = stringResource(id = R.string.other),
|
|
||||||
padding = screenPadding,
|
|
||||||
)
|
|
||||||
ConfigurationToggle(
|
ConfigurationToggle(
|
||||||
stringResource(R.string.always_on_vpn_support),
|
stringResource(R.string.always_on_vpn_support),
|
||||||
enabled = !uiState.settings.isAutoTunnelEnabled,
|
enabled = !uiState.settings.isAutoTunnelEnabled,
|
||||||
@@ -601,9 +624,27 @@ fun SettingsScreen(
|
|||||||
padding = screenPadding,
|
padding = screenPadding,
|
||||||
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
ConfigurationToggle(
|
||||||
|
stringResource(R.string.enable_app_lock),
|
||||||
|
enabled = true,
|
||||||
|
checked = pinExists.value,
|
||||||
|
padding = screenPadding,
|
||||||
|
onCheckChanged = {
|
||||||
|
if (pinExists.value) {
|
||||||
|
PinManager.clearPin()
|
||||||
|
pinExists.value = PinManager.pinExists()
|
||||||
|
} else {
|
||||||
|
navController.navigate(Screen.Lock.route)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 5.dp),
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -616,9 +657,6 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
|
||||||
Spacer(modifier = Modifier.weight(.17f))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,5 @@ data class SettingsUiState(
|
|||||||
val tunnels: List<TunnelConfig> = emptyList(),
|
val tunnels: List<TunnelConfig> = emptyList(),
|
||||||
val vpnState: VpnState = VpnState(),
|
val vpnState: VpnState = VpnState(),
|
||||||
val isLocationDisclosureShown: Boolean = true,
|
val isLocationDisclosureShown: Boolean = true,
|
||||||
val isBatteryOptimizeDisableShown: Boolean = false,
|
val isBatteryOptimizeDisableShown: Boolean = false
|
||||||
val loading: Boolean = true
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ constructor(
|
|||||||
tunnelState,
|
tunnelState,
|
||||||
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
|
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
|
||||||
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
|
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
|
||||||
false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
@@ -183,9 +182,10 @@ constructor(
|
|||||||
if (!uiState.value.settings.isKernelEnabled) {
|
if (!uiState.value.settings.isKernelEnabled) {
|
||||||
try {
|
try {
|
||||||
rootShell.start()
|
rootShell.start()
|
||||||
Timber.d("Root shell accepted!")
|
Timber.i("Root shell accepted!")
|
||||||
saveKernelMode(on = true)
|
saveKernelMode(on = true)
|
||||||
} catch (e: RootShell.RootShellException) {
|
} catch (e: RootShell.RootShellException) {
|
||||||
|
Timber.e(e)
|
||||||
saveKernelMode(on = false)
|
saveKernelMode(on = false)
|
||||||
return Result.Error(Event.Error.RootDenied)
|
return Result.Error(Event.Error.RootDenied)
|
||||||
}
|
}
|
||||||
@@ -194,4 +194,12 @@ constructor(
|
|||||||
}
|
}
|
||||||
return Result.Success(Unit)
|
return Result.Success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onToggleRestartOnPing() = viewModelScope.launch {
|
||||||
|
saveSettings(
|
||||||
|
uiState.value.settings.copy(
|
||||||
|
isPingEnabled = !uiState.value.settings.isPingEnabled,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.Intent.createChooser
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -20,10 +16,11 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.ArrowForward
|
import androidx.compose.material.icons.automirrored.rounded.ArrowForward
|
||||||
import androidx.compose.material.icons.rounded.Book
|
import androidx.compose.material.icons.rounded.Book
|
||||||
|
import androidx.compose.material.icons.rounded.FormatListNumbered
|
||||||
import androidx.compose.material.icons.rounded.Mail
|
import androidx.compose.material.icons.rounded.Mail
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@@ -45,21 +42,21 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.navigation.NavController
|
||||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SupportScreen(
|
fun SupportScreen(
|
||||||
viewModel: SupportViewModel = hiltViewModel(),
|
viewModel: SupportViewModel = hiltViewModel(),
|
||||||
padding: PaddingValues,
|
appViewModel: AppViewModel,
|
||||||
showSnackbarMessage: (String) -> Unit,
|
navController: NavController,
|
||||||
focusRequester: FocusRequester
|
focusRequester: FocusRequester
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -67,47 +64,14 @@ fun SupportScreen(
|
|||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
fun openWebPage(url: String) {
|
|
||||||
try {
|
|
||||||
val webpage: Uri = Uri.parse(url)
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
|
||||||
context.startActivity(intent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchEmail() {
|
|
||||||
try {
|
|
||||||
val intent =
|
|
||||||
Intent(Intent.ACTION_SENDTO).apply {
|
|
||||||
type = Constants.EMAIL_MIME_TYPE
|
|
||||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
|
|
||||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
|
||||||
}
|
|
||||||
startActivity(
|
|
||||||
context,
|
|
||||||
createChooser(intent, context.getString(R.string.email_chooser)),
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiState.loading) {
|
|
||||||
LoadingScreen()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxSize()
|
Modifier
|
||||||
.verticalScroll(rememberScrollState())
|
.fillMaxSize()
|
||||||
.focusable()
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(padding),
|
.focusable()
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
@@ -116,15 +80,19 @@ fun SupportScreen(
|
|||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier =
|
modifier =
|
||||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||||
Modifier.height(IntrinsicSize.Min)
|
Modifier
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.height(IntrinsicSize.Min)
|
||||||
.padding(top = 10.dp)
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(top = 10.dp)
|
||||||
} else {
|
} else {
|
||||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
Modifier
|
||||||
|
.fillMaxWidth(fillMaxWidth)
|
||||||
|
.padding(top = 20.dp)
|
||||||
})
|
})
|
||||||
.padding(bottom = 25.dp),
|
.padding(bottom = 25.dp),
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(20.dp)) {
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
|
val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.thank_you),
|
stringResource(R.string.thank_you),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
@@ -139,8 +107,10 @@ fun SupportScreen(
|
|||||||
modifier = Modifier.padding(bottom = 20.dp),
|
modifier = Modifier.padding(bottom = 20.dp),
|
||||||
)
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||||
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester),
|
modifier = Modifier
|
||||||
|
.padding(vertical = 5.dp)
|
||||||
|
.focusRequester(focusRequester),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@@ -148,19 +118,26 @@ fun SupportScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
val icon = Icons.Rounded.Book
|
||||||
|
Icon(icon, icon.name)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.docs_description),
|
stringResource(id = R.string.docs_description),
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp),
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
Icon(
|
||||||
|
forwardIcon,
|
||||||
|
forwardIcon.name
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
HorizontalDivider(
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.discord_url)) },
|
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) },
|
||||||
modifier = Modifier.padding(vertical = 5.dp),
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -169,9 +146,10 @@ fun SupportScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
|
val icon = ImageVector.vectorResource(R.drawable.discord)
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
icon,
|
||||||
stringResource(id = R.string.discord),
|
icon.name,
|
||||||
Modifier.size(25.dp),
|
Modifier.size(25.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
@@ -180,12 +158,18 @@ fun SupportScreen(
|
|||||||
modifier = Modifier.padding(start = 10.dp),
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
Icon(
|
||||||
|
forwardIcon,
|
||||||
|
forwardIcon.name
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
HorizontalDivider(
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { openWebPage(context.resources.getString(R.string.github_url)) },
|
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) },
|
||||||
modifier = Modifier.padding(vertical = 5.dp),
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -194,23 +178,30 @@ fun SupportScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
|
val icon = ImageVector.vectorResource(R.drawable.github)
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.github),
|
imageVector = icon,
|
||||||
stringResource(id = R.string.github),
|
icon.name,
|
||||||
Modifier.size(25.dp),
|
Modifier.size(25.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"Open an issue",
|
stringResource(id = R.string.open_issue),
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp),
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
Icon(
|
||||||
|
forwardIcon,
|
||||||
|
forwardIcon.name
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Divider(color = MaterialTheme.colorScheme.onBackground, thickness = 0.5.dp)
|
HorizontalDivider(
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { launchEmail() },
|
onClick = { appViewModel.launchEmail() },
|
||||||
modifier = Modifier.padding(vertical = 5.dp),
|
modifier = Modifier.padding(vertical = 5.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -219,14 +210,48 @@ fun SupportScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
val icon = Icons.Rounded.Mail
|
||||||
|
Icon(icon, icon.name)
|
||||||
Text(
|
Text(
|
||||||
stringResource(id = R.string.email_description),
|
stringResource(id = R.string.email_description),
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = 10.dp),
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(Icons.Rounded.ArrowForward, stringResource(id = R.string.go))
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,7 +263,7 @@ fun SupportScreen(
|
|||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.clickable {
|
Modifier.clickable {
|
||||||
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
|||||||
|
|
||||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||||
|
|
||||||
data class SupportUiState(val settings: Settings = Settings(), val loading: Boolean = true)
|
data class SupportUiState(val settings: Settings = Settings())
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class SupportViewModel @Inject constructor(private val settingsRepository: Setti
|
|||||||
val uiState =
|
val uiState =
|
||||||
settingsRepository
|
settingsRepository
|
||||||
.getSettingsFlow()
|
.getSettingsFlow()
|
||||||
.map { SupportUiState(it, false) }
|
.map { SupportUiState(it) }
|
||||||
.stateIn(
|
.stateIn(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT),
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
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 androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,11 +69,6 @@ fun WireguardAutoTunnelTheme(
|
|||||||
window.navigationBarColor = Color.Transparent.toArgb()
|
window.navigationBarColor = Color.Transparent.toArgb()
|
||||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
|
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
|
||||||
!darkTheme
|
!darkTheme
|
||||||
WindowCompat.getInsetsController(
|
|
||||||
window,
|
|
||||||
window.decorView,
|
|
||||||
)
|
|
||||||
.isAppearanceLightNavigationBars = !darkTheme
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
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 {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
|
|
||||||
|
const val BASE_LOG_FILE_NAME = "wgtunnel-logs"
|
||||||
|
const val LOG_BUFFER_SIZE = 3_000L
|
||||||
|
|
||||||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||||
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes
|
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
|
||||||
const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1000L // 30 minutes
|
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
|
||||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
|
||||||
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3000L
|
|
||||||
const val TOGGLE_TUNNEL_DELAY = 300L
|
const val TOGGLE_TUNNEL_DELAY = 300L
|
||||||
const val CONF_FILE_EXTENSION = ".conf"
|
const val CONF_FILE_EXTENSION = ".conf"
|
||||||
const val ZIP_FILE_EXTENSION = ".zip"
|
const val ZIP_FILE_EXTENSION = ".zip"
|
||||||
const val URI_CONTENT_SCHEME = "content"
|
const val URI_CONTENT_SCHEME = "content"
|
||||||
const val URI_PACKAGE_SCHEME = "package"
|
|
||||||
const val ALLOWED_FILE_TYPES = "*/*"
|
const val ALLOWED_FILE_TYPES = "*/*"
|
||||||
|
const val TEXT_MIME_TYPE = "text/plain"
|
||||||
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
||||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||||
const val EMAIL_MIME_TYPE = "message/rfc822"
|
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||||
@@ -19,4 +22,11 @@ object Constants {
|
|||||||
|
|
||||||
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
||||||
const val FOCUS_REQUEST_DELAY = 500L
|
const val FOCUS_REQUEST_DELAY = 500L
|
||||||
|
|
||||||
|
const val BACKUP_PING_HOST = "1.1.1.1"
|
||||||
|
const val PING_TIMEOUT = 5_000L
|
||||||
|
const val VPN_RESTART_DELAY = 1_000L
|
||||||
|
const val PING_INTERVAL = 60_000L
|
||||||
|
const val PING_COOLDOWN = PING_INTERVAL * 60 //one hour
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ sealed class Event {
|
|||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class ConfigParseError(val appendedMessage : String) : Error() {
|
||||||
|
override val message: String =
|
||||||
|
WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + (
|
||||||
|
if (appendedMessage != "") ": ${appendedMessage.trim()}" else "")
|
||||||
|
}
|
||||||
|
|
||||||
data object RootDenied : Error() {
|
data object RootDenied : Error() {
|
||||||
override val message: String
|
override val message: String
|
||||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ fun BroadcastReceiver.goAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.truncateWithEllipsis(allowedLength : Int) : String {
|
||||||
|
return if(this.length > allowedLength + 3) {
|
||||||
|
this.substring(0, allowedLength) + "***"
|
||||||
|
} else this
|
||||||
|
}
|
||||||
|
|
||||||
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
fun BigDecimal.toThreeDecimalPlaceString(): String {
|
||||||
val df = DecimalFormat("#.###")
|
val df = DecimalFormat("#.###")
|
||||||
return df.format(this)
|
return df.format(this)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.os.Environment
|
|||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.MediaStore.MediaColumns
|
import android.provider.MediaStore.MediaColumns
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
@@ -43,6 +44,31 @@ object FileUtils {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveFileToDownloads(context: Context, content: String, fileName: String) {
|
||||||
|
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||||
|
put(MediaStore.MediaColumns.MIME_TYPE, Constants.TEXT_MIME_TYPE)
|
||||||
|
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
}
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
if (uri != null) {
|
||||||
|
resolver.openOutputStream(uri).use { output ->
|
||||||
|
output?.write(content.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val target = File(
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
fileName
|
||||||
|
)
|
||||||
|
FileOutputStream(target).use { output ->
|
||||||
|
output.write(content.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun saveFilesToZip(context: Context, files: List<File>) {
|
fun saveFilesToZip(context: Context, files: List<File>) {
|
||||||
val zipOutputStream =
|
val zipOutputStream =
|
||||||
createDownloadsFileOutputStream(
|
createDownloadsFileOutputStream(
|
||||||
|
|||||||
@@ -19,7 +19,15 @@ object NumberUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun generateRandomTunnelName(): String {
|
fun generateRandomTunnelName(): String {
|
||||||
return "tunnel${(Math.random() * 100000).toInt()}"
|
return "tunnel${randomFive()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun randomFive() : Int {
|
||||||
|
return (Math.random() * 100000).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun randomThree() : Int {
|
||||||
|
return (Math.random() * 1000).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSecondsBetweenTimestampAndNow(epoch: Long): Long? {
|
fun getSecondsBetweenTimestampAndNow(epoch: Long): Long? {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class ReleaseTree : Timber.Tree() {
|
||||||
|
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||||
|
when(priority) {
|
||||||
|
Log.DEBUG -> return
|
||||||
|
}
|
||||||
|
super.log(priority,tag,message,t)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="640"
|
||||||
|
android:viewportHeight="640"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.0132159"
|
||||||
|
android:scaleY="1.0132159"
|
||||||
|
android:translateX="-4.229075"
|
||||||
|
android:translateY="-4.229075">
|
||||||
|
<path
|
||||||
|
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:fillColor="#53bdb6"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 382 B |
|
After Width: | Height: | Size: 698 B |
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,45 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="320dp"
|
||||||
|
android:height="180dp"
|
||||||
|
android:viewportWidth="640"
|
||||||
|
android:viewportHeight="640">
|
||||||
|
<group android:scaleX="0.6666667"
|
||||||
|
android:scaleY="0.6666667"
|
||||||
|
android:translateX="106.666664"
|
||||||
|
android:translateY="106.666664">
|
||||||
|
<group android:scaleX="0.315"
|
||||||
|
android:scaleY="0.56"
|
||||||
|
android:translateX="46.4"
|
||||||
|
android:translateY="140.8">
|
||||||
|
<path
|
||||||
|
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:fillColor="#53bdb6"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group android:scaleX="0.52644"
|
||||||
|
android:scaleY="0.20707752"
|
||||||
|
android:translateX="320.4"
|
||||||
|
android:translateY="70.26513">
|
||||||
|
<group android:translateY="132.92308">
|
||||||
|
<path android:pathData="M76.06154,-101.68616L87.95077,-101.68616L72.44308,0L55.606155,0L43.93846,-76.72615L32.196922,0L14.990769,0L0.6646154,-101.68616L13.513846,-101.68616L24.36923,-14.104615L36.553844,-89.870766L52.283077,-89.870766L63.876923,-14.104615Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
<path android:pathData="M135.30154,1.6984615Q123.11692,1.6984615,114.25539,-4.1723075Q105.393845,-10.0430765,100.55692,-21.821539Q95.72,-33.6,95.72,-50.88Q95.72,-67.643074,101.62769,-79.495384Q107.535385,-91.347694,117.246155,-97.32923Q126.956924,-103.31077,138.25539,-103.31077Q147.78154,-103.31077,154.42769,-100.61539Q161.07385,-97.92,167.35077,-92.16L159.59692,-84.11077Q154.94461,-88.46769,149.77539,-90.535385Q144.60616,-92.60307,138.32922,-92.60307Q130.42769,-92.60307,123.92923,-88.504616Q117.43077,-84.40615,113.44308,-75.10154Q109.45538,-65.79692,109.45538,-50.88Q109.45538,-29.095385,116.175385,-19.089231Q122.895386,-9.0830765,136.48308,-9.0830765Q146.7477,-9.0830765,155.83076,-13.9569235L155.83076,-44.97231L135.81847,-44.97231L134.26768,-55.606155L168.5323,-55.606155L168.5323,-7.163077Q160.70462,-2.88,152.98769,-0.59076923Q145.27077,1.6984615,135.30154,1.6984615Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
<path android:pathData="M348.30463,-90.683075L317.43692,-90.683075L317.43692,0L304.66153,0L304.66153,-90.683075L272.90768,-90.683075L272.90768,-101.68616L349.63385,-101.68616Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
<path android:pathData="M383.24924,-22.670769Q383.24924,-15.064615,386.3877,-11.630769Q389.52615,-8.196923,396.32,-8.196923Q402.44922,-8.196923,408.24615,-11.741538Q414.0431,-15.286154,417.36615,-20.52923L417.36615,-77.76L429.7723,-77.76L429.7723,0L419.2123,0L418.17847,-10.486154Q413.67386,-4.726154,406.99078,-1.5138462Q400.30768,1.6984615,393.2923,1.6984615Q382.14154,1.6984615,376.4923,-4.246154Q370.84308,-10.190769,370.84308,-21.193846L370.84308,-77.76L383.24924,-77.76Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
<path android:pathData="M459.84308,0L459.84308,-77.76L470.40308,-77.76L471.36307,-66.97846Q476.01538,-72.81231,483.0677,-76.098465Q490.12,-79.38461,497.06152,-79.38461Q508.2123,-79.38461,513.56616,-73.47692Q518.92,-67.56923,518.92,-56.49231L518.92,0L506.51385,0L506.51385,-47.335384Q506.51385,-56.049232,505.59076,-60.627693Q504.6677,-65.206154,501.82462,-67.42154Q498.98154,-69.636925,493.22153,-69.636925Q486.9446,-69.636925,481.36923,-65.76Q475.79385,-61.883076,472.24924,-56.64L472.24924,0Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
<path android:pathData="M548.8431,0L548.8431,-77.76L559.4031,-77.76L560.3631,-66.97846Q565.0154,-72.81231,572.0677,-76.098465Q579.12,-79.38461,586.0615,-79.38461Q597.2123,-79.38461,602.56616,-73.47692Q607.92,-67.56923,607.92,-56.49231L607.92,0L595.51385,0L595.51385,-47.335384Q595.51385,-56.049232,594.59076,-60.627693Q593.66766,-65.206154,590.8246,-67.42154Q587.98157,-69.636925,582.22156,-69.636925Q575.94464,-69.636925,570.3692,-65.76Q564.7938,-61.883076,561.2492,-56.64L561.2492,0Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
<path android:pathData="M647.59076,-34.486153Q647.88617,-25.772308,650.9877,-19.975384Q654.08923,-14.178461,659.1108,-11.409231Q664.1323,-8.64,670.3354,-8.64Q676.0954,-8.64,680.9323,-10.338462Q685.7692,-12.036923,691.0862,-15.655385L696.92,-7.4584618Q691.4554,-3.1753845,684.4031,-0.73846155Q677.35077,1.6984615,670.1877,1.6984615Q659.0369,1.6984615,650.9877,-3.323077Q642.9385,-8.344615,638.7662,-17.50154Q634.5939,-26.65846,634.5939,-38.76923Q634.5939,-50.51077,638.8031,-59.74154Q643.0123,-68.972305,650.6923,-74.17846Q658.3723,-79.38461,668.56305,-79.38461Q678.3108,-79.38461,685.43695,-74.80615Q692.56305,-70.22769,696.36615,-61.624615Q700.16925,-53.021538,700.16925,-41.28Q700.16925,-37.883076,699.87384,-34.486153ZM668.71075,-69.19385Q659.70154,-69.19385,654.0523,-62.88Q648.4031,-56.566154,647.6646,-44.086155L688.2062,-44.086155Q687.9846,-56.344616,682.81537,-62.76923Q677.6462,-69.19385,668.71075,-69.19385Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
<path android:pathData="M757.12,-19.2Q757.12,-13.735385,760.55383,-11.187693Q763.9877,-8.64,769.96924,-8.64Q776.24615,-8.64,783.04,-11.372308L786.3631,-2.2892308Q782.8185,-0.51692307,778.0923,0.59076923Q773.36615,1.6984615,767.90155,1.6984615Q761.0339,1.6984615,755.75385,-0.8861538Q750.4738,-3.4707692,747.5939,-8.344615Q744.71387,-13.218462,744.71387,-19.864614L744.71387,-99.24923L720.8615,-99.24923L720.8615,-109.07077L757.12,-109.07077Z"
|
||||||
|
android:fillColor="#53BDB6"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#3DDC84"
|
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,30 +1,16 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="640"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="640">
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
<group android:scaleX="0.6"
|
||||||
<aapt:attr name="android:fillColor">
|
android:scaleY="0.6"
|
||||||
<gradient
|
android:translateX="128"
|
||||||
android:endX="85.84757"
|
android:translateY="128">
|
||||||
android:endY="92.4963"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
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:fillType="nonZero"
|
android:fillColor="#53bdb6"
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
android:fillType="evenOdd"
|
||||||
android:strokeWidth="1"
|
android:strokeColor="#00000000"/>
|
||||||
android:strokeColor="#00000000" />
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="#000000"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_banner_background" />
|
<background android:drawable="@color/ic_banner_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_banner_foreground" />
|
<foreground android:drawable="@drawable/ic_banner_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<!-- support for adaptive theme icons -->
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<!-- support for adaptive theme icons -->
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 796 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.4 KiB |
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="ic_banner_background">#121212</color>
|
<color name="ic_banner_background">#1D1A20</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="ic_launcher_background">#1C1B20</color>
|
<color name="ic_launcher_background">#1D1A20</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -6,21 +6,21 @@
|
|||||||
<string name="watcher_channel_id">Watcher Channel</string>
|
<string name="watcher_channel_id">Watcher Channel</string>
|
||||||
<string name="watcher_channel_name">Watcher Notification Channel</string>
|
<string name="watcher_channel_name">Watcher Notification Channel</string>
|
||||||
<string name="foreground_file">FOREGROUND_FILE</string>
|
<string name="foreground_file">FOREGROUND_FILE</string>
|
||||||
<string name="github_url">https://github.com/zaneschepke/wgtunnel/issues</string>
|
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
|
||||||
<string name="docs_url">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
|
<string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
|
||||||
<string name="privacy_policy_url">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
|
<string name="privacy_policy_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
|
||||||
<string name="error_file_extension">File is not a .conf or .zip</string>
|
<string name="error_file_extension">File is not a .conf or .zip</string>
|
||||||
<string name="turn_off_tunnel">Action requires tunnel off</string>
|
<string name="turn_off_tunnel">Action requires tunnel off</string>
|
||||||
<string name="no_tunnels">No tunnels added yet!</string>
|
<string name="no_tunnels">No tunnels added yet!</string>
|
||||||
<string name="tunnel_exists">Tunnel name already exists</string>
|
<string name="tunnel_exists">Tunnel name already exists</string>
|
||||||
<string name="discord_url">https://discord.gg/rbRRNh6H7V</string>
|
<string name="discord_url" translatable="false">https://discord.gg/rbRRNh6H7V</string>
|
||||||
<string name="watcher_notification_title">Watcher Service</string>
|
<string name="watcher_notification_title">Watcher Service</string>
|
||||||
<string name="watcher_notification_text_active">Monitoring network state changes: active</string>
|
<string name="watcher_notification_text_active">Monitoring network state changes: active</string>
|
||||||
<string name="watcher_notification_text_paused">Monitoring network state changes: paused</string>
|
<string name="watcher_notification_text_paused">Monitoring network state changes: paused</string>
|
||||||
<string name="tunnel_start_title">VPN Connected</string>
|
<string name="tunnel_start_title">VPN Connected</string>
|
||||||
<string name="tunnel_start_text">Connected to tunnel -</string>
|
<string name="tunnel_start_text">Connected to tunnel -</string>
|
||||||
<string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string>
|
<string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string>
|
||||||
<string name="notification_permission_required">Notifications permission is required for the app to work properly.</string>
|
<string name="notification_permission_required">Notifications permission required.</string>
|
||||||
<string name="open_settings">Open Settings</string>
|
<string name="open_settings">Open Settings</string>
|
||||||
<string name="add_trusted_ssid">Add trusted wifi name</string>
|
<string name="add_trusted_ssid">Add trusted wifi name</string>
|
||||||
<string name="tunnels">Tunnels</string>
|
<string name="tunnels">Tunnels</string>
|
||||||
@@ -39,15 +39,13 @@
|
|||||||
<string name="thank_you">Thank you for using WG Tunnel!</string>
|
<string name="thank_you">Thank you for using WG Tunnel!</string>
|
||||||
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
||||||
<string name="trusted_ssid_value_description">Submit SSID</string>
|
<string name="trusted_ssid_value_description">Submit SSID</string>
|
||||||
<string name="config_validation">[Interface]</string>
|
<string name="config_validation" translatable="false">[Interface]</string>
|
||||||
<string name="add_tunnels_text">Add from file or zip</string>
|
<string name="add_tunnels_text">Add from file or zip</string>
|
||||||
<string name="open_file">File Open</string>
|
<string name="open_file">File Open</string>
|
||||||
<string name="add_from_qr">Add from QR code</string>
|
<string name="add_from_qr">Add from QR code</string>
|
||||||
<string name="qr_scan">QR Scan</string>
|
<string name="qr_scan">QR Scan</string>
|
||||||
<string name="tunnel_edit">Tunnel Edit</string>
|
<string name="tunnel_edit">Tunnel Edit</string>
|
||||||
<string name="tunnel_name">Tunnel Name</string>
|
<string name="tunnel_name">Tunnel Name</string>
|
||||||
<string name="edit">Edit</string>
|
|
||||||
<string name="delete">Delete</string>
|
|
||||||
<string name="add_tunnel">Add Tunnel</string>
|
<string name="add_tunnel">Add Tunnel</string>
|
||||||
<string name="exclude">Exclude</string>
|
<string name="exclude">Exclude</string>
|
||||||
<string name="include">Include</string>
|
<string name="include">Include</string>
|
||||||
@@ -73,7 +71,7 @@
|
|||||||
<string name="last_handshake">Last handshake</string>
|
<string name="last_handshake">Last handshake</string>
|
||||||
<string name="name">Name</string>
|
<string name="name">Name</string>
|
||||||
<string name="restart">Restart Tunnel</string>
|
<string name="restart">Restart Tunnel</string>
|
||||||
<string name="vpn_connection_failed">VPN Connection Failed</string>
|
<string name="vpn_connection_failed">Connection failed</string>
|
||||||
<string name="failed_connection_to">Failed connection to -</string>
|
<string name="failed_connection_to">Failed connection to -</string>
|
||||||
<string name="initial_connection_failure_message">Attempting to connect to server after 30 seconds of no response.</string>
|
<string name="initial_connection_failure_message">Attempting to connect to server after 30 seconds of no response.</string>
|
||||||
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
|
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
|
||||||
@@ -90,7 +88,7 @@
|
|||||||
<string name="clear_icon">Clear Icon</string>
|
<string name="clear_icon">Clear Icon</string>
|
||||||
<string name="search_icon">Search Icon</string>
|
<string name="search_icon">Search Icon</string>
|
||||||
<string name="attempt_connection">Attempting connection..</string>
|
<string name="attempt_connection">Attempting connection..</string>
|
||||||
<string name="vpn_starting">VPN Starting</string>
|
<string name="vpn_starting">VPN starting</string>
|
||||||
<string name="db_name">wg-tunnel-db</string>
|
<string name="db_name">wg-tunnel-db</string>
|
||||||
<string name="scanning_qr">Scanning for QR</string>
|
<string name="scanning_qr">Scanning for QR</string>
|
||||||
<string name="qr_result_failed">QR scan failed</string>
|
<string name="qr_result_failed">QR scan failed</string>
|
||||||
@@ -105,11 +103,9 @@
|
|||||||
<string name="default_vpn_on">Primary VPN on</string>
|
<string name="default_vpn_on">Primary VPN on</string>
|
||||||
<string name="default_vpn_off">Primary VPN off</string>
|
<string name="default_vpn_off">Primary VPN off</string>
|
||||||
<string name="create_import">Create from scratch</string>
|
<string name="create_import">Create from scratch</string>
|
||||||
<string name="set_primary">Set primary</string>
|
|
||||||
<string name="turn_off_auto">Action requires auto-tunnel disabled</string>
|
<string name="turn_off_auto">Action requires auto-tunnel disabled</string>
|
||||||
<string name="turn_on_tunnel">Action requires active tunnel</string>
|
<string name="turn_on_tunnel">Action requires active tunnel</string>
|
||||||
<string name="add_peer">Add peer</string>
|
<string name="add_peer">Add peer</string>
|
||||||
<string name="info">Info</string>
|
|
||||||
<string name="done">Done</string>
|
<string name="done">Done</string>
|
||||||
<string name="interface_">Interface</string>
|
<string name="interface_">Interface</string>
|
||||||
<string name="rotate_keys">Rotate keys</string>
|
<string name="rotate_keys">Rotate keys</string>
|
||||||
@@ -119,7 +115,6 @@
|
|||||||
<string name="comma_separated_list">comma separated list</string>
|
<string name="comma_separated_list">comma separated list</string>
|
||||||
<string name="listen_port">Listen port</string>
|
<string name="listen_port">Listen port</string>
|
||||||
<string name="random">(random)</string>
|
<string name="random">(random)</string>
|
||||||
<string name="auto">(auto)</string>
|
|
||||||
<string name="optional">(optional)</string>
|
<string name="optional">(optional)</string>
|
||||||
<string name="optional_no_recommend">(optional, not recommended)</string>
|
<string name="optional_no_recommend">(optional, not recommended)</string>
|
||||||
<string name="preshared_key">Pre-shared key</string>
|
<string name="preshared_key">Pre-shared key</string>
|
||||||
@@ -140,16 +135,12 @@
|
|||||||
<string name="exported_configs_message">Exported configs to downloads</string>
|
<string name="exported_configs_message">Exported configs to downloads</string>
|
||||||
<string name="status">status</string>
|
<string name="status">status</string>
|
||||||
<string name="tunnel_on_wifi">Tunnel on untrusted wifi</string>
|
<string name="tunnel_on_wifi">Tunnel on untrusted wifi</string>
|
||||||
<string name="my_email">zanecschepke@gmail.com</string>
|
<string name="my_email" translatable="false">support@zaneschepke.com</string>
|
||||||
<string name="email_subject">WG Tunnel Support</string>
|
<string name="email_subject">WG Tunnel Support</string>
|
||||||
<string name="email_chooser">Send an email…</string>
|
<string name="email_chooser">Send an email…</string>
|
||||||
<string name="go">go</string>
|
<string name="go">go</string>
|
||||||
<string name="docs_description">Read the docs (WIP)</string>
|
<string name="docs_description">Read the docs (WIP)</string>
|
||||||
<string name="discord_description">Join the community</string>
|
<string name="discord_description">Join the community</string>
|
||||||
<string name="discord">Discord</string>
|
|
||||||
<string name="docs">Docs</string>
|
|
||||||
<string name="github">GitHub</string>
|
|
||||||
<string name="email">Email</string>
|
|
||||||
<string name="email_description">Send me an email</string>
|
<string name="email_description">Send me an email</string>
|
||||||
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
|
<string name="support_help_text">If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available:</string>
|
||||||
<string name="kernel">Kernel</string>
|
<string name="kernel">Kernel</string>
|
||||||
@@ -167,4 +158,26 @@
|
|||||||
<string name="delete_tunnel">Delete tunnel</string>
|
<string name="delete_tunnel">Delete tunnel</string>
|
||||||
<string name="delete_tunnel_message">Are you sure you would like to delete this tunnel?</string>
|
<string name="delete_tunnel_message">Are you sure you would like to delete this tunnel?</string>
|
||||||
<string name="yes">Yes</string>
|
<string name="yes">Yes</string>
|
||||||
|
<string name="resume">Resume</string>
|
||||||
|
<string name="pause">Pause</string>
|
||||||
|
<string name="paused">paused</string>
|
||||||
|
<string name="active">active</string>
|
||||||
|
<string name="tunneling_apps">Tunneling apps</string>
|
||||||
|
<string name="included">included</string>
|
||||||
|
<string name="excluded">excluded</string>
|
||||||
|
<string name="all">all</string>
|
||||||
|
<string name="always_on_disabled">Always-on VPN attempted to start a tunnel, but this feature is disabled in settings.</string>
|
||||||
|
<string name="no_email_detected">No email app detected</string>
|
||||||
|
<string name="no_browser_detected">No browser detected</string>
|
||||||
|
<string name="logs_saved">Logs saved to downloads</string>
|
||||||
|
<string name="open_issue">Open an issue</string>
|
||||||
|
<string name="read_logs">Read the logs</string>
|
||||||
|
<string name="auto">(auto)</string>
|
||||||
|
<string name="config_parse_error">Failed to parse config</string>
|
||||||
|
<string name="incorrect_pin">Pin is incorrect</string>
|
||||||
|
<string name="pin_created">Pin successfully created</string>
|
||||||
|
<string name="enter_pin">Enter your pin</string>
|
||||||
|
<string name="create_pin">Create pin</string>
|
||||||
|
<string name="enable_app_lock">Enabled app lock</string>
|
||||||
|
<string name="restart_on_ping">Restart on ping fail</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Theme.WireguardAutoTunnel" parent="@style/Theme.AppCompat.NoActionBar">
|
<style name="Theme.WireguardAutoTunnel" parent="@style/Theme.AppCompat.NoActionBar">
|
||||||
<item name="android:windowBackground">@color/black_background</item>
|
<item name="android:windowBackground">@color/black_background</item>
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 38 KiB |
@@ -1,16 +1,8 @@
|
|||||||
buildscript {
|
|
||||||
dependencies {
|
|
||||||
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
|
|
||||||
classpath(libs.google.services)
|
|
||||||
classpath(libs.firebase.crashlytics.gradle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
alias(libs.plugins.hilt.android) apply false
|
alias(libs.plugins.hilt.android) apply false
|
||||||
kotlin("plugin.serialization").version(libs.versions.kotlin).apply(false)
|
alias(libs.plugins.kotlinxSerialization) apply false
|
||||||
alias(libs.plugins.ksp) apply false
|
alias(libs.plugins.ksp) apply false
|
||||||
|
alias(libs.plugins.androidLibrary) apply false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import org.gradle.api.invocation.Gradle
|
import org.gradle.api.invocation.Gradle
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
object BuildHelper {
|
object BuildHelper {
|
||||||
private fun getCurrentFlavor(gradle: Gradle): String {
|
private fun getCurrentFlavor(gradle: Gradle): String {
|
||||||
@@ -21,6 +22,17 @@ object BuildHelper {
|
|||||||
return flavor
|
return flavor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getLocalProperty(key: String, file: String = "local.properties"): String? {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
val localProperties = File(file)
|
||||||
|
if (localProperties.isFile) {
|
||||||
|
java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8).use { reader ->
|
||||||
|
properties.load(reader)
|
||||||
|
}
|
||||||
|
} else return null
|
||||||
|
return properties.getProperty(key)
|
||||||
|
}
|
||||||
|
|
||||||
fun isGeneralFlavor(gradle: Gradle): Boolean {
|
fun isGeneralFlavor(gradle: Gradle): Boolean {
|
||||||
return getCurrentFlavor(gradle) == "general"
|
return getCurrentFlavor(gradle) == "general"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
object Constants {
|
object Constants {
|
||||||
const val VERSION_NAME = "3.3.3"
|
const val VERSION_NAME = "3.3.9"
|
||||||
const val JVM_TARGET = "17"
|
const val JVM_TARGET = "17"
|
||||||
const val VERSION_CODE = 33300
|
const val VERSION_CODE = 33900
|
||||||
const val TARGET_SDK = 34
|
const val TARGET_SDK = 34
|
||||||
const val MIN_SDK = 26
|
const val MIN_SDK = 26
|
||||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||||
const val APP_NAME = "wgtunnel"
|
const val APP_NAME = "wgtunnel"
|
||||||
const val COMPOSE_COMPILER_EXTENSION_VERSION = "1.5.7"
|
const val COMPOSE_COMPILER_EXTENSION_VERSION = "1.5.10"
|
||||||
|
|
||||||
|
|
||||||
const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD"
|
const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD"
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ default_platform(:android)
|
|||||||
|
|
||||||
platform :android do
|
platform :android do
|
||||||
|
|
||||||
|
desc 'Deploy a new internal version to the Google Play Store'
|
||||||
|
lane :internal do
|
||||||
|
gradle(task: "clean bundleGeneralRelease")
|
||||||
|
upload_to_play_store(track: 'internal', skip_upload_apk: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Deploy an alpha version to the Google Play"
|
||||||
|
lane :alpha do
|
||||||
|
gradle(task: "clean bundleGeneralRelease")
|
||||||
|
upload_to_play_store(track: 'alpha', skip_upload_apk: true)
|
||||||
|
end
|
||||||
|
|
||||||
desc "Deploy a beta version to the Google Play"
|
desc "Deploy a beta version to the Google Play"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
gradle(task: "clean bundleGeneralRelease")
|
gradle(task: "clean bundleGeneralRelease")
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Enhancements:
|
||||||
|
- Fixed create config not saving
|
||||||
|
- Bumped versions
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
What's new:
|
||||||
|
- This is a CI testing version
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
What's new:
|
||||||
|
- Auto-start on reboot for Always-On VPN kernel mode
|
||||||
|
- Support for adaptive theme icons
|
||||||
|
- Fix notification icons, tile icon
|
||||||
|
- Fix AndroidTV icons
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
What's new:
|
||||||
|
- Improve first launch flow
|
||||||
|
- Switch to wireguard lib fork
|
||||||
|
- Request VPN permission on first VPN start
|
||||||
|
- Bump versions
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
What's new:
|
||||||
|
- Tunnel display UI bug fix
|
||||||