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: Android CI Tag Deployment
|
||||
name: Android CI Tag Deployment (Pre-release)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
- '*.*.*-**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -13,10 +14,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
KEY_STORE_PATH: ${{ secrets.KEY_STORE_PATH }}
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
KEY_STORE_FILE: 'android_keystore.jks'
|
||||
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
|
||||
GH_USER: ${{ secrets.GH_USER }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -35,10 +39,16 @@ jobs:
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
with:
|
||||
fileName: 'android_keystore.jks'
|
||||
fileDir: ${{ github.workspace }}/app/keystore/
|
||||
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
|
||||
@@ -52,10 +62,13 @@ jobs:
|
||||
- name: Get apk path
|
||||
id: apk-path
|
||||
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get version code
|
||||
run: |
|
||||
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
|
||||
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
|
||||
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4.0.0
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: wgtunnel
|
||||
path: ${{ steps.apk-path.outputs.path }}
|
||||
@@ -70,18 +83,17 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
# 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 }}
|
||||
name: Release ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
prerelease: true
|
||||
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 Beta track 🚀
|
||||
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://www.amazon.com/gp/product/B0CFGGL7WK)
|
||||
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://ko-fi.com/N4N8NMJN2)
|
||||
|
||||
</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
|
||||
* 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
|
||||
|
||||
```
|
||||
$ git clone https://github.com/zaneschepke/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
|
||||
```
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.hilt.android)
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
alias(libs.plugins.kotlinxSerialization)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
@@ -111,8 +111,7 @@ android {
|
||||
create("general") {
|
||||
dimension = Constants.TYPE
|
||||
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle)) {
|
||||
apply(plugin = "com.google.gms.google-services")
|
||||
apply(plugin = "com.google.firebase.crashlytics")
|
||||
//any plugins general specific
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,9 +132,12 @@ android {
|
||||
val generalImplementation by configurations
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(project(":logcatter"))
|
||||
|
||||
implementation(libs.androidx.core.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.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
@@ -156,13 +158,14 @@ dependencies {
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.manifest)
|
||||
|
||||
// wg
|
||||
// get tunnel lib from github packages or mavenLocal
|
||||
implementation(libs.tunnel)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
||||
// logging
|
||||
implementation(libs.timber)
|
||||
|
||||
|
||||
// compose navigation
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
@@ -172,7 +175,6 @@ dependencies {
|
||||
ksp(libs.hilt.android.compiler)
|
||||
|
||||
// accompanist
|
||||
implementation(libs.accompanist.systemuicontroller)
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
implementation(libs.accompanist.drawablepainter)
|
||||
@@ -193,17 +195,13 @@ dependencies {
|
||||
// serialization
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
// firebase crashlytics
|
||||
generalImplementation(platform(libs.firebase.bom))
|
||||
generalImplementation(libs.google.firebase.crashlytics.ktx)
|
||||
generalImplementation(libs.google.firebase.analytics.ktx)
|
||||
|
||||
// barcode scanning
|
||||
implementation(libs.zxing.android.embedded)
|
||||
implementation(libs.zxing.core)
|
||||
|
||||
// bio
|
||||
implementation(libs.androidx.biometric.ktx)
|
||||
implementation(libs.pin.lock.compose)
|
||||
|
||||
// shortcuts
|
||||
implementation(libs.androidx.core)
|
||||
|
||||
@@ -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.platform.app.InstrumentationRegistry
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.Queries
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -26,33 +27,7 @@ class MigrationTest {
|
||||
helper.createDatabase(dbName, 4).apply {
|
||||
// Database has schema version 1. Insert some data using SQL queries.
|
||||
// You can't use DAO classes because they expect the latest schema.
|
||||
execSQL(
|
||||
"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(Queries.createDefaultSettings())
|
||||
execSQL(
|
||||
"INSERT INTO TunnelConfig (name, wg_quick)" + " VALUES ('hello', 'hello')",
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
@@ -23,7 +22,6 @@
|
||||
<!--foreground service exempt android 14-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
|
||||
<!--foreground service permissions-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
@@ -84,7 +82,8 @@
|
||||
android:screenOrientation="fullSensor"
|
||||
android:stateNotNeeded="true"
|
||||
android:theme="@style/zxing_CaptureTheme"
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
tools:ignore="DiscouragedApi" />
|
||||
<activity
|
||||
android:name=".service.shortcut.ShortcutsActivity"
|
||||
android:enabled="true"
|
||||
@@ -96,12 +95,12 @@
|
||||
android:name=".service.foreground.ForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted|specialUse"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
tools:node="merge" />
|
||||
<service
|
||||
android:name=".service.tile.TunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/shield"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="WG Tunnel"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
@@ -119,7 +118,7 @@
|
||||
android:name=".service.foreground.WireGuardTunnelService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted|specialUse"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:persistent="true"
|
||||
tools:node="merge">
|
||||
@@ -134,7 +133,7 @@
|
||||
android:name=".service.foreground.WireGuardConnectivityWatcherService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted|specialUse"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:persistent="true"
|
||||
android:stopWithTask="false"
|
||||
tools:node="merge" />
|
||||
@@ -145,7 +144,6 @@
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
||||
<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.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.service.quicksettings.TileService
|
||||
import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile
|
||||
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
@HiltAndroidApp
|
||||
class WireGuardAutoTunnel : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) else Timber.plant(ReleaseTree())
|
||||
PinManager.initialize(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var instance: WireGuardAutoTunnel
|
||||
private set
|
||||
@@ -24,9 +27,9 @@ class WireGuardAutoTunnel : Application() {
|
||||
return instance.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
}
|
||||
|
||||
fun requestTileServiceStateUpdate() {
|
||||
fun requestTileServiceStateUpdate(context : Context) {
|
||||
TileService.requestListeningState(
|
||||
instance,
|
||||
context,
|
||||
ComponentName(instance, TunnelControlTile::class.java),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
|
||||
@Database(
|
||||
entities = [Settings::class, TunnelConfig::class],
|
||||
version = 5,
|
||||
version = 6,
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 1, to = 2),
|
||||
@@ -22,6 +22,10 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
from = 4,
|
||||
to = 5,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 5,
|
||||
to = 6,
|
||||
),
|
||||
],
|
||||
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.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class DataStoreManager(private val context: Context) {
|
||||
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] }
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -51,6 +51,11 @@ data class Settings(
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isAutoTunnelPaused: Boolean = false,
|
||||
@ColumnInfo(
|
||||
name = "is_ping_enabled",
|
||||
defaultValue = "false",
|
||||
)
|
||||
var isPingEnabled: Boolean = false,
|
||||
) {
|
||||
fun isTunnelConfigDefault(tunnelConfig: TunnelConfig): Boolean {
|
||||
return if (defaultTunnel != null) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.AppDatabase
|
||||
import com.zaneschepke.wireguardautotunnel.data.DatabaseCallback
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -23,6 +24,7 @@ class DatabaseModule {
|
||||
context.getString(R.string.db_name),
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.addCallback(DatabaseCallback())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.util.goAsync
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -14,10 +16,17 @@ class BootReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject lateinit var tunnelConfigRepository: TunnelConfigRepository
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) = 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!!)
|
||||
} 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)
|
||||
stopSelf()
|
||||
} catch (e: Exception) {
|
||||
Timber.d("Service stopped without being started: ${e.message}")
|
||||
Timber.e(e)
|
||||
}
|
||||
isServiceStarted = false
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import timber.log.Timber
|
||||
|
||||
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(
|
||||
context: Context,
|
||||
) {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.os.SystemClock
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
@@ -28,10 +24,11 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.net.InetAddress
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
private val foregroundId = 122
|
||||
@@ -122,45 +119,18 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
launchWatcherNotification(getString(R.string.watcher_notification_text_paused))
|
||||
}
|
||||
|
||||
// TODO could this be restarting service in a bad state?
|
||||
// try to start task again if killed
|
||||
override fun onTaskRemoved(rootIntent: Intent) {
|
||||
Timber.d("Task Removed called")
|
||||
val restartServiceIntent = Intent(rootIntent)
|
||||
val restartServicePendingIntent: PendingIntent =
|
||||
PendingIntent.getService(
|
||||
this,
|
||||
1,
|
||||
restartServiceIntent,
|
||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
applicationContext.getSystemService(Context.ALARM_SERVICE)
|
||||
val alarmService: AlarmManager =
|
||||
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
alarmService.set(
|
||||
AlarmManager.ELAPSED_REALTIME,
|
||||
SystemClock.elapsedRealtime() + 1000,
|
||||
restartServicePendingIntent,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun initWakeLock() {
|
||||
val isBatterySaverOn =
|
||||
withContext(lifecycleScope.coroutineContext) {
|
||||
settingsRepository.getSettings().isBatterySaverEnabled
|
||||
}
|
||||
private fun initWakeLock() {
|
||||
wakeLock =
|
||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||
if (isBatterySaverOn) {
|
||||
Timber.d("Initiating wakelock with timeout")
|
||||
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||
} else {
|
||||
Timber.d("Initiating wakelock with zero timeout")
|
||||
acquire(Constants.DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||
}
|
||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
|
||||
try {
|
||||
Timber.i("Initiating wakelock with 10 min timeout")
|
||||
acquire(Constants.BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT)
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelWatcherJob() {
|
||||
@@ -174,33 +144,40 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val setting = settingsRepository.getSettings()
|
||||
launch {
|
||||
Timber.d("Starting wifi watcher")
|
||||
Timber.i("Starting wifi watcher")
|
||||
watchForWifiConnectivityChanges()
|
||||
}
|
||||
if (setting.isTunnelOnMobileDataEnabled) {
|
||||
launch {
|
||||
Timber.d("Starting mobile data watcher")
|
||||
Timber.i("Starting mobile data watcher")
|
||||
watchForMobileDataConnectivityChanges()
|
||||
}
|
||||
}
|
||||
if (setting.isTunnelOnEthernetEnabled) {
|
||||
launch {
|
||||
Timber.d("Starting ethernet data watcher")
|
||||
Timber.i("Starting ethernet data watcher")
|
||||
watchForEthernetConnectivityChanges()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.d("Starting vpn state watcher")
|
||||
Timber.i("Starting vpn state watcher")
|
||||
watchForVpnConnectivityChanges()
|
||||
}
|
||||
launch {
|
||||
Timber.d("Starting settings watcher")
|
||||
Timber.i("Starting settings watcher")
|
||||
watchForSettingsChanges()
|
||||
}
|
||||
if(setting.isPingEnabled) {
|
||||
launch {
|
||||
Timber.i("Starting ping watcher")
|
||||
watchForPingFailure()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
Timber.d("Starting management watcher")
|
||||
Timber.i("Starting management watcher")
|
||||
manageVpn()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +185,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
mobileDataService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Mobile data connection")
|
||||
Timber.i("Gained Mobile data connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
@@ -219,19 +196,53 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
networkEventsFlow.value.copy(
|
||||
isMobileDataConnected = true,
|
||||
)
|
||||
Timber.d("Mobile data capabilities changed")
|
||||
Timber.i("Mobile data capabilities changed")
|
||||
}
|
||||
is NetworkStatus.Unavailable -> {
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
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() {
|
||||
settingsRepository.getSettingsFlow().collect {
|
||||
if (networkEventsFlow.value.settings.isAutoTunnelPaused != it.isAutoTunnelPaused) {
|
||||
@@ -269,14 +280,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
ethernetService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Ethernet connection")
|
||||
Timber.i("Gained Ethernet connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
)
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.d("Ethernet capabilities changed")
|
||||
Timber.i("Ethernet capabilities changed")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = true,
|
||||
@@ -287,7 +298,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
networkEventsFlow.value.copy(
|
||||
isEthernetConnected = false,
|
||||
)
|
||||
Timber.d("Lost Ethernet connection")
|
||||
Timber.i("Lost Ethernet connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,20 +308,20 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
wifiService.networkStatus.collect {
|
||||
when (it) {
|
||||
is NetworkStatus.Available -> {
|
||||
Timber.d("Gained Wi-Fi connection")
|
||||
Timber.i("Gained Wi-Fi connection")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
}
|
||||
is NetworkStatus.CapabilitiesChanged -> {
|
||||
Timber.d("Wifi capabilities changed")
|
||||
Timber.i("Wifi capabilities changed")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = true,
|
||||
)
|
||||
val ssid = wifiService.getNetworkName(it.networkCapabilities) ?: ""
|
||||
Timber.d("Detected SSID: $ssid")
|
||||
Timber.i("Detected SSID: $ssid")
|
||||
networkEventsFlow.value =
|
||||
networkEventsFlow.value.copy(
|
||||
currentNetworkSSID = ssid,
|
||||
@@ -321,16 +332,15 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
networkEventsFlow.value.copy(
|
||||
isWifiConnected = false,
|
||||
)
|
||||
Timber.d("Lost Wi-Fi connection")
|
||||
Timber.i("Lost Wi-Fi connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO clean this up
|
||||
private suspend fun manageVpn() {
|
||||
networkEventsFlow.collectLatest {
|
||||
Timber.i("New watcher state: $it")
|
||||
val autoTunnel = "Auto-tunnel watcher"
|
||||
if (!it.settings.isAutoTunnelPaused && it.settings.defaultTunnel != null) {
|
||||
delay(Constants.TOGGLE_TUNNEL_DELAY)
|
||||
when {
|
||||
@@ -338,7 +348,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
it.settings.isTunnelOnEthernetEnabled &&
|
||||
!it.isVpnConnected)) -> {
|
||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||
Timber.i("Condition 1 met")
|
||||
Timber.i("$autoTunnel condition 1 met")
|
||||
}
|
||||
(!it.isEthernetConnected &&
|
||||
it.settings.isTunnelOnMobileDataEnabled &&
|
||||
@@ -346,14 +356,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
it.isMobileDataConnected &&
|
||||
!it.isVpnConnected) -> {
|
||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||
Timber.i("Condition 2 met")
|
||||
Timber.i("$autoTunnel condition 2 met")
|
||||
}
|
||||
(!it.isEthernetConnected &&
|
||||
!it.settings.isTunnelOnMobileDataEnabled &&
|
||||
!it.isWifiConnected &&
|
||||
it.isVpnConnected) -> {
|
||||
ServiceManager.stopVpnService(this)
|
||||
Timber.i("Condition 3 met")
|
||||
Timber.i("$autoTunnel condition 3 met")
|
||||
}
|
||||
(!it.isEthernetConnected &&
|
||||
it.isWifiConnected &&
|
||||
@@ -361,31 +371,31 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
||||
it.settings.isTunnelOnWifiEnabled &&
|
||||
(!it.isVpnConnected)) -> {
|
||||
ServiceManager.startVpnServiceForeground(this, it.settings.defaultTunnel!!)
|
||||
Timber.i("Condition 4 met")
|
||||
Timber.i("$autoTunnel condition 4 met")
|
||||
}
|
||||
(!it.isEthernetConnected &&
|
||||
(it.isWifiConnected &&
|
||||
it.settings.trustedNetworkSSIDs.contains(it.currentNetworkSSID)) &&
|
||||
(it.isVpnConnected)) -> {
|
||||
ServiceManager.stopVpnService(this)
|
||||
Timber.i("Condition 5 met")
|
||||
Timber.i("$autoTunnel condition 5 met")
|
||||
}
|
||||
(!it.isEthernetConnected &&
|
||||
(it.isWifiConnected &&
|
||||
!it.settings.isTunnelOnWifiEnabled &&
|
||||
(it.isVpnConnected))) -> {
|
||||
ServiceManager.stopVpnService(this)
|
||||
Timber.i("Condition 6 met")
|
||||
Timber.i("$autoTunnel condition 6 met")
|
||||
}
|
||||
(!it.isEthernetConnected &&
|
||||
!it.isWifiConnected &&
|
||||
!it.isMobileDataConnected &&
|
||||
(it.isVpnConnected)) -> {
|
||||
ServiceManager.stopVpnService(this)
|
||||
Timber.i("Condition 7 met")
|
||||
Timber.i("$autoTunnel condition 7 met")
|
||||
}
|
||||
else -> {
|
||||
Timber.i("No condition met")
|
||||
Timber.i("$autoTunnel no condition met")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
stopService(extras)
|
||||
}
|
||||
} 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 tunnels = tunnelConfigRepository.getAll()
|
||||
if (settings.isAlwaysOnVpnEnabled) {
|
||||
@@ -84,6 +84,8 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
tunnelName = tunnel.name
|
||||
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?) {
|
||||
super.stopService(extras)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
|
||||
@@ -94,7 +94,7 @@ class WireGuardNotification @Inject constructor(@ApplicationContext private val
|
||||
.setOngoing(onGoing)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setShowWhen(showTimestamp)
|
||||
.setSmallIcon(R.mipmap.ic_launcher_foreground)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,7 @@ class TunnelControlTile() : TileService() {
|
||||
setUnavailable()
|
||||
return@collect
|
||||
}
|
||||
tunnelName =
|
||||
it.name.ifBlank {
|
||||
tunnelName = it.name.run {
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.defaultTunnel != null) {
|
||||
TunnelConfig.from(settings.defaultTunnel!!).name
|
||||
@@ -72,15 +71,18 @@ class TunnelControlTile() : TileService() {
|
||||
unlockAndRun {
|
||||
scope.launch {
|
||||
try {
|
||||
val tunnelConfig =
|
||||
tunnelConfigRepository.getAll().first { it.name == tunnelName }
|
||||
val defaultTunnel = settingsRepository.getSettings().defaultTunnel
|
||||
val config = defaultTunnel ?: run {
|
||||
val tunnelConfigs = tunnelConfigRepository.getAll()
|
||||
return@run tunnelConfigs.find { it.name == tunnelName }
|
||||
}
|
||||
toggleWatcherServicePause()
|
||||
if (vpnService.getState() == Tunnel.State.UP) {
|
||||
ServiceManager.stopVpnService(this@TunnelControlTile)
|
||||
} else {
|
||||
ServiceManager.startVpnServiceForeground(
|
||||
this@TunnelControlTile,
|
||||
tunnelConfig.toString(),
|
||||
config.toString(),
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -2,9 +2,12 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Statistics
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
|
||||
data class VpnState(
|
||||
val status: Tunnel.State = Tunnel.State.DOWN,
|
||||
val name: String = "",
|
||||
val config: Config? = null,
|
||||
val statistics: Statistics? = null
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ import javax.inject.Inject
|
||||
class WireGuardTunnel
|
||||
@Inject
|
||||
constructor(
|
||||
@Userspace private val userspaceBackend: Backend,
|
||||
@Userspace private val userspaceBackend : Backend,
|
||||
@Kernel private val kernelBackend: Backend,
|
||||
private val settingsRepository: SettingsRepository
|
||||
) : VpnService {
|
||||
@@ -63,6 +63,7 @@ constructor(
|
||||
stopTunnelOnConfigChange(tunnelConfig)
|
||||
emitTunnelName(tunnelConfig.name)
|
||||
config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
|
||||
emitTunnelConfig(config)
|
||||
val state =
|
||||
backend.setState(
|
||||
this,
|
||||
@@ -71,7 +72,7 @@ constructor(
|
||||
)
|
||||
emitTunnelState(state)
|
||||
state
|
||||
} catch (e: Exception) {
|
||||
} catch (e: BackendException) {
|
||||
Timber.e("Failed to start tunnel with error: ${e.message}")
|
||||
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) {
|
||||
if (getState() == State.UP && _vpnState.value.name != tunnelConfig.name) {
|
||||
stopTunnel()
|
||||
@@ -129,7 +138,7 @@ constructor(
|
||||
override fun onStateChange(state: State) {
|
||||
val tunnel = this
|
||||
emitTunnelState(state)
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate(WireGuardAutoTunnel.instance)
|
||||
if (state == State.UP) {
|
||||
statsJob =
|
||||
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
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarData
|
||||
@@ -18,44 +19,50 @@ import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScreen
|
||||
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.pinlock.PinLockScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||
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.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import xyz.teamgravity.pin_lock_compose.PinLock
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -64,81 +71,92 @@ class MainActivity : AppCompatActivity() {
|
||||
@Inject
|
||||
lateinit var dataStoreManager: DataStoreManager
|
||||
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@OptIn(
|
||||
ExperimentalPermissionsApi::class,
|
||||
)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
|
||||
|
||||
// load preferences into memory and init data
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
dataStoreManager.init()
|
||||
if (settingsRepository.getAll().isEmpty()) {
|
||||
settingsRepository.save(com.zaneschepke.wireguardautotunnel.data.model.Settings())
|
||||
}
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
} catch (e: IOException) {
|
||||
Timber.e("Failed to load preferences")
|
||||
dataStoreManager.init()
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate(this@MainActivity)
|
||||
val settings = settingsRepository.getSettings()
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
ServiceManager.startWatcherService(application.applicationContext)
|
||||
}
|
||||
}
|
||||
setContent {
|
||||
// val activityViewModel = hiltViewModel<ActivityViewModel>()
|
||||
|
||||
val appViewModel = hiltViewModel<AppViewModel>()
|
||||
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
|
||||
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 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 {
|
||||
TransparentSystemBars()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val notificationPermissionState =
|
||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
||||
fun requestNotificationPermission() {
|
||||
if (
|
||||
!notificationPermissionState.status.isGranted &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
||||
) {
|
||||
notificationPermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
|
||||
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
|
||||
val vpnActivityResultState =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = {
|
||||
val accepted = (it.resultCode == RESULT_OK)
|
||||
if (accepted) {
|
||||
vpnIntent = null
|
||||
}
|
||||
},
|
||||
LaunchedEffect(Unit) {
|
||||
appViewModel.setNotificationPermissionAccepted(
|
||||
notificationPermissionState?.status?.isGranted ?: true,
|
||||
)
|
||||
LaunchedEffect(vpnIntent) {
|
||||
if (vpnIntent != null) {
|
||||
vpnActivityResultState.launch(vpnIntent)
|
||||
} else {
|
||||
requestNotificationPermission()
|
||||
}
|
||||
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) appViewModel.readLogCatOutput()
|
||||
}
|
||||
|
||||
fun showSnackBarMessage(message: String) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val result =
|
||||
snackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
actionLabel = applicationContext.getString(R.string.okay),
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
when (result) {
|
||||
SnackbarResult.ActionPerformed,
|
||||
SnackbarResult.Dismissed -> {
|
||||
snackbarHostState.currentSnackbarData?.dismiss()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(appUiState.snackbarMessageConsumed) {
|
||||
if (!appUiState.snackbarMessageConsumed) {
|
||||
showSnackBarMessage(StringValue.DynamicString(appUiState.snackbarMessage))
|
||||
appViewModel.snackbarMessageConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,65 +167,45 @@ class MainActivity : AppCompatActivity() {
|
||||
snackbarData.visuals.message,
|
||||
isRtl = false,
|
||||
containerColor =
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
2.dp,
|
||||
),
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
2.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.focusable().focusProperties { up = focusRequester },
|
||||
bottomBar =
|
||||
if (vpnIntent == null && notificationPermissionState.status.isGranted) {
|
||||
{
|
||||
BottomNavBar(
|
||||
navController,
|
||||
listOf(
|
||||
Screen.Main.navItem,
|
||||
Screen.Settings.navItem,
|
||||
Screen.Support.navItem,
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
{}
|
||||
},
|
||||
//TODO refactor
|
||||
modifier = Modifier
|
||||
.focusable()
|
||||
.focusProperties { when(navBackStackEntry?.destination?.route) {
|
||||
Screen.Lock.route -> Unit
|
||||
else -> up = focusRequester }
|
||||
},
|
||||
bottomBar = {
|
||||
BottomNavBar(
|
||||
navController,
|
||||
listOf(
|
||||
Screen.Main.navItem,
|
||||
Screen.Settings.navItem,
|
||||
Screen.Support.navItem,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
if (vpnIntent != null) {
|
||||
PermissionRequestFailedScreen(
|
||||
padding = padding,
|
||||
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
|
||||
message = getString(R.string.vpn_permission_required),
|
||||
getString(R.string.retry),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
if (!notificationPermissionState.status.isGranted) {
|
||||
PermissionRequestFailedScreen(
|
||||
padding = padding,
|
||||
onRequestAgain = {
|
||||
val intentSettings =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intentSettings.data =
|
||||
Uri.fromParts(
|
||||
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) {
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination =
|
||||
(if (PinManager.pinExists()) Screen.Lock.route else Screen.Main.route),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(padding)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
composable(
|
||||
Screen.Main.route,
|
||||
) {
|
||||
MainScreen(
|
||||
padding = padding,
|
||||
focusRequester = focusRequester,
|
||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||
appViewModel = appViewModel,
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
@@ -215,34 +213,37 @@ class MainActivity : AppCompatActivity() {
|
||||
Screen.Settings.route,
|
||||
) {
|
||||
SettingsScreen(
|
||||
padding = padding,
|
||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||
focusRequester = focusRequester,
|
||||
appViewModel = appViewModel,
|
||||
navController = navController,
|
||||
focusRequester = focusRequester
|
||||
)
|
||||
}
|
||||
composable(
|
||||
Screen.Support.route,
|
||||
) {
|
||||
SupportScreen(
|
||||
padding = padding,
|
||||
focusRequester = focusRequester,
|
||||
showSnackbarMessage = { message -> showSnackBarMessage(message) },
|
||||
appViewModel = appViewModel,
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
composable(Screen.Support.Logs.route) {
|
||||
LogsScreen(appViewModel)
|
||||
}
|
||||
composable("${Screen.Config.route}/{id}") {
|
||||
val id = it.arguments?.getString("id")
|
||||
if (!id.isNullOrBlank()) {
|
||||
// https://dagger.dev/hilt/view-model#assisted-injection
|
||||
ConfigScreen(
|
||||
navController = navController,
|
||||
id = id,
|
||||
showSnackbarMessage = { message ->
|
||||
showSnackBarMessage(message)
|
||||
},
|
||||
appViewModel = appViewModel,
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
}
|
||||
}
|
||||
composable(Screen.Lock.route) {
|
||||
PinLockScreen(navController = navController, appViewModel = appViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ sealed class Screen(val route: String) {
|
||||
route = route,
|
||||
icon = Icons.Rounded.QuestionMark,
|
||||
)
|
||||
data object Logs : Screen("support/logs")
|
||||
}
|
||||
|
||||
data object Config : Screen("config")
|
||||
data object Lock : Screen("lock")
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ fun ClickableIconButton(
|
||||
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = stringResource(R.string.delete),
|
||||
contentDescription = icon.name,
|
||||
modifier =
|
||||
Modifier.size(ButtonDefaults.IconSize).weight(1f, false).clickable {
|
||||
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(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(.60f),
|
||||
) {
|
||||
icon()
|
||||
Text(text)
|
||||
@@ -65,6 +64,7 @@ fun RowListItem(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
//TODO change these to string resources
|
||||
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||
val peerTx = statistics.peer(it)!!.txBytes
|
||||
val peerRx = statistics.peer(it)!!.rxBytes
|
||||
|
||||
@@ -6,18 +6,33 @@ import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
|
||||
@Composable
|
||||
fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavItem>) {
|
||||
val backStackEntry = navController.currentBackStackEntryAsState()
|
||||
|
||||
var showBottomBar by rememberSaveable { mutableStateOf(true) }
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
|
||||
//TODO find a better way to hide nav bar
|
||||
showBottomBar = when (navBackStackEntry?.destination?.route) {
|
||||
Screen.Lock.route -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
NavigationBar(
|
||||
containerColor = 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
|
||||
|
||||
NavigationBarItem(
|
||||
|
||||
@@ -19,7 +19,6 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
@@ -33,7 +32,6 @@ fun CustomSnackBar(
|
||||
isRtl: Boolean = true,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Snackbar(
|
||||
containerColor = containerColor,
|
||||
modifier =
|
||||
@@ -51,9 +49,10 @@ fun CustomSnackBar(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
) {
|
||||
val icon = Icons.Rounded.Info
|
||||
Icon(
|
||||
Icons.Rounded.Info,
|
||||
contentDescription = stringResource(R.string.info),
|
||||
icon,
|
||||
contentDescription = icon.name,
|
||||
tint = Color.White,
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
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.Refresh
|
||||
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.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
@@ -51,7 +50,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
@@ -63,6 +61,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
@@ -73,6 +72,7 @@ import androidx.navigation.NavController
|
||||
import com.google.accompanist.drawablepainter.DrawablePainter
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
|
||||
@@ -86,16 +86,14 @@ import kotlinx.coroutines.delay
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@OptIn(
|
||||
ExperimentalComposeUiApi::class,
|
||||
ExperimentalMaterial3Api::class,
|
||||
ExperimentalFoundationApi::class,
|
||||
)
|
||||
@Composable
|
||||
fun ConfigScreen(
|
||||
viewModel: ConfigViewModel = hiltViewModel(),
|
||||
focusRequester: FocusRequester,
|
||||
navController: NavController,
|
||||
showSnackbarMessage: (String) -> Unit,
|
||||
appViewModel: AppViewModel,
|
||||
id: String
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -129,13 +127,16 @@ fun ConfigScreen(
|
||||
val fillMaxWidth = .85f
|
||||
val screenPadding = 5.dp
|
||||
|
||||
val applicationButtonText = {
|
||||
"Tunneling apps: " +
|
||||
val applicationButtonText = buildAnnotatedString {
|
||||
append(stringResource(id = R.string.tunneling_apps))
|
||||
append(": ")
|
||||
if (uiState.isAllApplicationsEnabled) {
|
||||
"all"
|
||||
append(stringResource(id = R.string.all))
|
||||
} else {
|
||||
"${uiState.checkedPackageNames.size} " +
|
||||
(if (uiState.include) "included" else "excluded")
|
||||
append("${uiState.checkedPackageNames.size} ")
|
||||
(if (uiState.include) append(stringResource(id = R.string.included)) else append(
|
||||
stringResource(id = R.string.excluded),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,13 +146,13 @@ fun ConfigScreen(
|
||||
showAuthPrompt = false
|
||||
isAuthenticated = true
|
||||
},
|
||||
onError = { error ->
|
||||
onError = {
|
||||
showAuthPrompt = false
|
||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
},
|
||||
onFailure = {
|
||||
showAuthPrompt = false
|
||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -161,20 +162,24 @@ fun ConfigScreen(
|
||||
remember(uiState.packages) {
|
||||
uiState.packages.sortedBy { viewModel.getPackageLabel(it) }
|
||||
}
|
||||
AlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
||||
BasicAlertDialog(onDismissRequest = { showApplicationsDialog = false }) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier
|
||||
.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
@@ -187,8 +192,9 @@ fun ConfigScreen(
|
||||
if (!uiState.isAllApplicationsEnabled) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
@@ -219,8 +225,9 @@ fun ConfigScreen(
|
||||
}
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
@@ -236,7 +243,9 @@ fun ConfigScreen(
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxSize().padding(5.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(5.dp),
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) {
|
||||
val drawable =
|
||||
@@ -248,9 +257,10 @@ fun ConfigScreen(
|
||||
modifier = Modifier.size(50.dp, 50.dp),
|
||||
)
|
||||
} else {
|
||||
val icon = Icons.Rounded.Android
|
||||
Icon(
|
||||
Icons.Rounded.Android,
|
||||
stringResource(id = R.string.edit),
|
||||
icon,
|
||||
icon.name,
|
||||
modifier = Modifier.size(50.dp, 50.dp),
|
||||
)
|
||||
}
|
||||
@@ -262,9 +272,9 @@ fun ConfigScreen(
|
||||
Checkbox(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
checked =
|
||||
(uiState.checkedPackageNames.contains(
|
||||
pack.packageName
|
||||
)),
|
||||
(uiState.checkedPackageNames.contains(
|
||||
pack.packageName
|
||||
)),
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
viewModel.onAddCheckedPackage(pack.packageName)
|
||||
@@ -279,7 +289,9 @@ fun ConfigScreen(
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(onClick = { showApplicationsDialog = false }) {
|
||||
@@ -299,7 +311,7 @@ fun ConfigScreen(
|
||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||
FloatingActionButton(
|
||||
modifier =
|
||||
Modifier.padding(bottom = 90.dp).onFocusChanged {
|
||||
Modifier.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
@@ -308,10 +320,10 @@ fun ConfigScreen(
|
||||
viewModel.onSaveAllChanges().let {
|
||||
when (it) {
|
||||
is Result.Success -> {
|
||||
showSnackbarMessage(it.data.message)
|
||||
appViewModel.showSnackbarMessage(it.data.message)
|
||||
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,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier.verticalScroll(rememberScrollState()).weight(1f, true).fillMaxSize(),
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.weight(1f, true)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
@@ -340,16 +355,20 @@ fun ConfigScreen(
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
||||
Modifier
|
||||
.fillMaxHeight(fillMaxHeight)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth)
|
||||
})
|
||||
.padding(top = 50.dp, bottom = 10.dp),
|
||||
.padding(bottom = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp).focusGroup(),
|
||||
modifier = Modifier
|
||||
.padding(15.dp)
|
||||
.focusGroup(),
|
||||
) {
|
||||
SectionTitle(
|
||||
stringResource(R.string.interface_),
|
||||
@@ -361,10 +380,14 @@ fun ConfigScreen(
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.name),
|
||||
hint = stringResource(R.string.tunnel_name).lowercase(),
|
||||
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth().clickable { showAuthPrompt = true },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { showAuthPrompt = true },
|
||||
value = uiState.interfaceProxy.privateKey,
|
||||
visualTransformation =
|
||||
if ((id == Constants.MANUAL_TUNNEL_CONFIG_ID) || isAuthenticated)
|
||||
@@ -392,7 +415,9 @@ fun ConfigScreen(
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth().focusRequester(FocusRequester.Default),
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(FocusRequester.Default),
|
||||
value = uiState.interfaceProxy.publicKey,
|
||||
enabled = false,
|
||||
onValueChange = {},
|
||||
@@ -425,7 +450,9 @@ fun ConfigScreen(
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.addresses),
|
||||
hint = stringResource(R.string.comma_separated_list),
|
||||
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(3 / 5f)
|
||||
.padding(end = 5.dp),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.listenPort,
|
||||
@@ -443,7 +470,9 @@ fun ConfigScreen(
|
||||
keyboardActions = keyboardActions,
|
||||
label = stringResource(R.string.dns_servers),
|
||||
hint = stringResource(R.string.comma_separated_list),
|
||||
modifier = Modifier.fillMaxWidth(3 / 5f).padding(end = 5.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(3 / 5f)
|
||||
.padding(end = 5.dp),
|
||||
)
|
||||
ConfigurationTextBox(
|
||||
value = uiState.interfaceProxy.mtu,
|
||||
@@ -456,11 +485,13 @@ fun ConfigScreen(
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(onClick = { showApplicationsDialog = true }) {
|
||||
Text(applicationButtonText())
|
||||
Text(applicationButtonText.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -473,7 +504,9 @@ fun ConfigScreen(
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier.fillMaxHeight(fillMaxHeight).fillMaxWidth(fillMaxWidth)
|
||||
Modifier
|
||||
.fillMaxHeight(fillMaxHeight)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth)
|
||||
})
|
||||
@@ -482,19 +515,24 @@ fun ConfigScreen(
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(horizontal = 15.dp).padding(bottom = 10.dp),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 15.dp)
|
||||
.padding(bottom = 10.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 5.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
stringResource(R.string.peer),
|
||||
padding = screenPadding,
|
||||
)
|
||||
IconButton(onClick = { viewModel.onDeletePeer(index) }) {
|
||||
Icon(Icons.Rounded.Delete, stringResource(R.string.delete))
|
||||
val icon = Icons.Rounded.Delete
|
||||
Icon(icon, icon.name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,7 +608,9 @@ fun ConfigScreen(
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxSize().padding(bottom = 140.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = 140.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
||||
@@ -7,8 +7,10 @@ import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wireguard.config.BadConfigException
|
||||
import com.wireguard.config.Config
|
||||
import com.wireguard.config.Interface
|
||||
import com.wireguard.config.ParseException
|
||||
import com.wireguard.config.Peer
|
||||
import com.wireguard.crypto.Key
|
||||
import com.wireguard.crypto.KeyPair
|
||||
@@ -30,6 +32,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -151,7 +154,7 @@ constructor(
|
||||
viewModelScope.launch {
|
||||
if (tunnelConfig != null) {
|
||||
saveConfig(tunnelConfig).join()
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
|
||||
updateSettingsDefaultTunnel(tunnelConfig)
|
||||
}
|
||||
}
|
||||
@@ -206,15 +209,19 @@ constructor(
|
||||
val peerList = buildPeerListFromProxyPeers()
|
||||
val wgInterface = buildInterfaceListFromProxyInterface()
|
||||
val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build()
|
||||
val tunnelConfig =
|
||||
_uiState.value.tunnel?.copy(
|
||||
val tunnelConfig = when(uiState.value.tunnel) {
|
||||
null -> TunnelConfig(name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString())
|
||||
else -> uiState.value.tunnel!!.copy(
|
||||
name = _uiState.value.tunnelName,
|
||||
wgQuick = config.toWgQuickString(),
|
||||
)
|
||||
}
|
||||
updateTunnelConfig(tunnelConfig)
|
||||
Result.Success(Event.Message.ConfigSaved)
|
||||
} 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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -39,25 +36,24 @@ import androidx.compose.material.icons.filled.QrCode
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
import androidx.compose.material.icons.rounded.Bolt
|
||||
import androidx.compose.material.icons.rounded.Circle
|
||||
import androidx.compose.material.icons.rounded.CopyAll
|
||||
import androidx.compose.material.icons.rounded.Delete
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.Info
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.typography
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -77,10 +73,10 @@ import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
@@ -93,6 +89,7 @@ import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
||||
@@ -104,19 +101,18 @@ import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
|
||||
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
|
||||
import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
viewModel: MainViewModel = hiltViewModel(),
|
||||
padding: PaddingValues,
|
||||
appViewModel: AppViewModel,
|
||||
focusRequester: FocusRequester,
|
||||
showSnackbarMessage: (String) -> Unit,
|
||||
navController: NavController
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
@@ -132,18 +128,13 @@ fun MainScreen(
|
||||
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(uiState.loading) {
|
||||
if (!uiState.loading && WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
LaunchedEffect(Unit) {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.loading) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val tunnelFileImportResultLauncher =
|
||||
rememberLauncherForActivityResult(
|
||||
object : ActivityResultContracts.GetContent() {
|
||||
@@ -173,7 +164,7 @@ fun MainScreen(
|
||||
name.startsWith(Constants.ANDROID_TV_EXPLORER_STUB)
|
||||
}
|
||||
) {
|
||||
showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||
appViewModel.showSnackbarMessage(Event.Error.FileExplorerRequired.message)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
@@ -183,7 +174,7 @@ fun MainScreen(
|
||||
scope.launch {
|
||||
viewModel.onTunnelFileSelected(data).let {
|
||||
when (it) {
|
||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
is Result.Success -> {}
|
||||
}
|
||||
}
|
||||
@@ -198,7 +189,7 @@ fun MainScreen(
|
||||
viewModel.onTunnelQrResult(it.contents).let { result ->
|
||||
when (result) {
|
||||
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) {
|
||||
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(
|
||||
modifier =
|
||||
Modifier.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) selectedTunnel = null
|
||||
},
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.End,
|
||||
topBar = {
|
||||
if (uiState.settings.isAutoTunnelEnabled)
|
||||
TopAppBar(
|
||||
title = {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier.requiredWidth(LocalConfiguration.current.screenWidthDp.dp)
|
||||
.padding(end = 5.dp),
|
||||
) {
|
||||
Row {
|
||||
Icon(
|
||||
Icons.Rounded.Bolt,
|
||||
stringResource(id = R.string.auto),
|
||||
modifier = Modifier.size(25.dp),
|
||||
tint =
|
||||
if (uiState.settings.isAutoTunnelPaused) Color.Gray
|
||||
else mint,
|
||||
)
|
||||
Text(
|
||||
"Auto-tunneling: ${if (uiState.settings.isAutoTunnelPaused) "paused" else "active"}",
|
||||
style = typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 10.dp),
|
||||
)
|
||||
}
|
||||
if (uiState.settings.isAutoTunnelPaused)
|
||||
TextButton(
|
||||
onClick = { viewModel.resumeAutoTunneling() },
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
) {
|
||||
Text("Resume")
|
||||
}
|
||||
else
|
||||
TextButton(
|
||||
onClick = { viewModel.pauseAutoTunneling() },
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
) {
|
||||
Text("Pause")
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
Modifier.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
selectedTunnel = null
|
||||
},
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.End,
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
visible = isVisible.value,
|
||||
@@ -323,18 +276,17 @@ fun MainScreen(
|
||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||
FloatingActionButton(
|
||||
modifier =
|
||||
(if (
|
||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||
uiState.tunnels.isEmpty()
|
||||
)
|
||||
Modifier.focusRequester(focusRequester)
|
||||
else Modifier)
|
||||
.padding(bottom = 90.dp)
|
||||
.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
},
|
||||
(if (
|
||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||
uiState.tunnels.isEmpty()
|
||||
)
|
||||
Modifier.focusRequester(focusRequester)
|
||||
else Modifier)
|
||||
.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
},
|
||||
onClick = { showBottomSheet = true },
|
||||
containerColor = fobColor,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
@@ -347,12 +299,13 @@ fun MainScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
) { innerPadding ->
|
||||
) {
|
||||
AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
@@ -365,12 +318,13 @@ fun MainScreen(
|
||||
// Sheet content
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable {
|
||||
showBottomSheet = false
|
||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||
}
|
||||
.padding(10.dp),
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
showBottomSheet = false
|
||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||
}
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.FileOpen,
|
||||
@@ -383,26 +337,27 @@ fun MainScreen(
|
||||
)
|
||||
}
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable {
|
||||
scope.launch {
|
||||
showBottomSheet = false
|
||||
val scanOptions = ScanOptions()
|
||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||
scanOptions.setOrientationLocked(true)
|
||||
scanOptions.setPrompt(
|
||||
context.getString(R.string.scanning_qr)
|
||||
)
|
||||
scanOptions.setBeepEnabled(false)
|
||||
scanOptions.captureActivity =
|
||||
CaptureActivityPortrait::class.java
|
||||
scanLauncher.launch(scanOptions)
|
||||
}
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
scope.launch {
|
||||
showBottomSheet = false
|
||||
val scanOptions = ScanOptions()
|
||||
scanOptions.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||
scanOptions.setOrientationLocked(true)
|
||||
scanOptions.setPrompt(
|
||||
context.getString(R.string.scanning_qr),
|
||||
)
|
||||
scanOptions.setBeepEnabled(false)
|
||||
scanOptions.captureActivity =
|
||||
CaptureActivityPortrait::class.java
|
||||
scanLauncher.launch(scanOptions)
|
||||
}
|
||||
.padding(10.dp),
|
||||
}
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.QrCode,
|
||||
@@ -415,17 +370,18 @@ fun MainScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable {
|
||||
showBottomSheet = false
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
||||
)
|
||||
}
|
||||
.padding(10.dp),
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
showBottomSheet = false
|
||||
navController.navigate(
|
||||
"${Screen.Config.route}/${Constants.MANUAL_TUNNEL_CONFIG_ID}",
|
||||
)
|
||||
}
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Create,
|
||||
@@ -444,15 +400,62 @@ fun MainScreen(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.fillMaxHeight(.90f)
|
||||
.overscroll(ScrollableDefaults.overscrollEffect())
|
||||
.padding(innerPadding),
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.overscroll(ScrollableDefaults.overscrollEffect()),
|
||||
state = rememberLazyListState(0, uiState.tunnels.count()),
|
||||
userScrollEnabled = true,
|
||||
reverseLayout = true,
|
||||
reverseLayout = false,
|
||||
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(
|
||||
uiState.tunnels,
|
||||
key = { tunnel -> tunnel.id },
|
||||
@@ -460,7 +463,7 @@ fun MainScreen(
|
||||
val leadingIconColor =
|
||||
(if (
|
||||
uiState.vpnState.name == tunnel.name &&
|
||||
uiState.vpnState.status == Tunnel.State.UP
|
||||
uiState.vpnState.status == Tunnel.State.UP
|
||||
) {
|
||||
uiState.vpnState.statistics
|
||||
?.mapPeerStats()
|
||||
@@ -471,6 +474,7 @@ fun MainScreen(
|
||||
statuses?.any { it == HandshakeStatus.STALE } == true -> corn
|
||||
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true ->
|
||||
Color.Gray
|
||||
|
||||
else -> {
|
||||
Color.Gray
|
||||
}
|
||||
@@ -487,24 +491,28 @@ fun MainScreen(
|
||||
Icons.Rounded.Star,
|
||||
stringResource(R.string.status),
|
||||
tint = leadingIconColor,
|
||||
modifier = Modifier.padding(end = 10.dp).size(20.dp),
|
||||
modifier = Modifier
|
||||
.padding(end = 10.dp)
|
||||
.size(20.dp),
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
Icons.Rounded.Circle,
|
||||
stringResource(R.string.status),
|
||||
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 = {
|
||||
if (
|
||||
(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
|
||||
}
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
@@ -514,7 +522,7 @@ fun MainScreen(
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
if (
|
||||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
(uiState.vpnState.name == tunnel.name)
|
||||
(uiState.vpnState.name == tunnel.name)
|
||||
) {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
@@ -528,7 +536,7 @@ fun MainScreen(
|
||||
rowButton = {
|
||||
if (
|
||||
tunnel.id == selectedTunnel?.id &&
|
||||
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||
!WireGuardAutoTunnel.isRunningOnAndroidTv()
|
||||
) {
|
||||
Row {
|
||||
if (!uiState.settings.isTunnelConfigDefault(tunnel)) {
|
||||
@@ -536,9 +544,9 @@ fun MainScreen(
|
||||
onClick = {
|
||||
if (
|
||||
uiState.settings.isAutoTunnelEnabled &&
|
||||
!uiState.settings.isAutoTunnelPaused
|
||||
!uiState.settings.isAutoTunnelPaused
|
||||
) {
|
||||
showSnackbarMessage(
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
)
|
||||
} else {
|
||||
@@ -546,9 +554,10 @@ fun MainScreen(
|
||||
}
|
||||
},
|
||||
) {
|
||||
val icon = Icons.Rounded.Star
|
||||
Icon(
|
||||
Icons.Rounded.Star,
|
||||
stringResource(id = R.string.set_primary),
|
||||
icon,
|
||||
icon.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -556,12 +565,12 @@ fun MainScreen(
|
||||
onClick = {
|
||||
if (
|
||||
uiState.settings.isAutoTunnelEnabled &&
|
||||
uiState.settings.isTunnelConfigDefault(
|
||||
tunnel,
|
||||
) &&
|
||||
!uiState.settings.isAutoTunnelPaused
|
||||
uiState.settings.isTunnelConfigDefault(
|
||||
tunnel,
|
||||
) &&
|
||||
!uiState.settings.isAutoTunnelPaused
|
||||
) {
|
||||
showSnackbarMessage(
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
)
|
||||
} 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(
|
||||
modifier = Modifier.focusable(),
|
||||
onClick = { showDeleteTunnelAlertDialog = true },
|
||||
) {
|
||||
Icon(Icons.Rounded.Delete, stringResource(id = R.string.delete))
|
||||
val icon = Icons.Rounded.Delete
|
||||
Icon(icon, icon.name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -604,7 +622,7 @@ fun MainScreen(
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (uiState.settings.isAutoTunnelEnabled) {
|
||||
showSnackbarMessage(
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.AutoTunnelOffAction.message,
|
||||
)
|
||||
} else {
|
||||
@@ -613,9 +631,10 @@ fun MainScreen(
|
||||
}
|
||||
},
|
||||
) {
|
||||
val icon = Icons.Rounded.Star
|
||||
Icon(
|
||||
Icons.Rounded.Star,
|
||||
stringResource(id = R.string.set_primary),
|
||||
icon,
|
||||
icon.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -624,26 +643,27 @@ fun MainScreen(
|
||||
onClick = {
|
||||
if (
|
||||
uiState.vpnState.status == Tunnel.State.UP &&
|
||||
(uiState.vpnState.name == tunnel.name)
|
||||
(uiState.vpnState.name == tunnel.name)
|
||||
) {
|
||||
expanded.value = !expanded.value
|
||||
} else {
|
||||
showSnackbarMessage(
|
||||
Event.Message.TunnelOnAction.message
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Message.TunnelOnAction.message,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
||||
val icon = Icons.Rounded.Info
|
||||
Icon(icon, icon.name)
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (
|
||||
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,
|
||||
)
|
||||
} else {
|
||||
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(
|
||||
onClick = {
|
||||
if (
|
||||
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,
|
||||
)
|
||||
} else {
|
||||
selectedTunnel = tunnel
|
||||
showDeleteTunnelAlertDialog = true
|
||||
}
|
||||
},
|
||||
) {
|
||||
val icon = Icons.Rounded.Delete
|
||||
Icon(
|
||||
Icons.Rounded.Delete,
|
||||
stringResource(id = R.string.delete),
|
||||
icon,
|
||||
icon.name
|
||||
)
|
||||
}
|
||||
TunnelSwitch()
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.zxing.common.StringUtils
|
||||
import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
@@ -49,7 +50,6 @@ constructor(
|
||||
tunnelConfigRepository.getTunnelConfigsFlow(),
|
||||
vpnService.vpnState,
|
||||
) { settings, tunnels, vpnState ->
|
||||
validateWatcherServiceState(settings)
|
||||
MainUiState(settings, tunnels, vpnState, false)
|
||||
}
|
||||
.stateIn(
|
||||
@@ -58,13 +58,6 @@ constructor(
|
||||
MainUiState(),
|
||||
)
|
||||
|
||||
private fun validateWatcherServiceState(settings: Settings) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (settings.isAutoTunnelEnabled) {
|
||||
ServiceManager.startWatcherService(application.applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopWatcherService() =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ServiceManager.stopWatcherService(application.applicationContext)
|
||||
@@ -72,16 +65,17 @@ constructor(
|
||||
|
||||
fun onDelete(tunnel: TunnelConfig) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (tunnelConfigRepository.count() == 1) {
|
||||
val settings = settingsRepository.getSettings()
|
||||
val isDefault = settings.isTunnelConfigDefault(tunnel)
|
||||
if (tunnelConfigRepository.count() == 1 || isDefault) {
|
||||
stopWatcherService()
|
||||
val settings = settingsRepository.getSettings()
|
||||
settings.defaultTunnel = null
|
||||
settings.isAutoTunnelEnabled = false
|
||||
settings.isAlwaysOnVpnEnabled = false
|
||||
saveSettings(settings)
|
||||
}
|
||||
tunnelConfigRepository.delete(tunnel)
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +100,7 @@ constructor(
|
||||
|
||||
fun onTunnelStop() =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Timber.d("Stopping active tunnel")
|
||||
Timber.i("Stopping active tunnel")
|
||||
ServiceManager.stopVpnService(application.applicationContext)
|
||||
}
|
||||
|
||||
@@ -122,6 +116,7 @@ constructor(
|
||||
addTunnel(tunnelConfig)
|
||||
Result.Success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
Result.Error(Event.Error.InvalidQrCode)
|
||||
}
|
||||
}
|
||||
@@ -158,6 +153,7 @@ constructor(
|
||||
return Result.Error(Event.Error.InvalidFileExtension)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return Result.Error(Event.Error.FileReadFailed)
|
||||
}
|
||||
}
|
||||
@@ -190,8 +186,9 @@ constructor(
|
||||
}
|
||||
|
||||
private suspend fun addTunnel(tunnelConfig: TunnelConfig) {
|
||||
val firstTunnel = tunnelConfigRepository.count() == 0
|
||||
saveTunnel(tunnelConfig)
|
||||
WireGuardAutoTunnel.requestTileServiceStateUpdate()
|
||||
if(firstTunnel) WireGuardAutoTunnel.requestTileServiceStateUpdate(application)
|
||||
}
|
||||
|
||||
fun pauseAutoTunneling() =
|
||||
@@ -249,6 +246,7 @@ constructor(
|
||||
return try {
|
||||
fileName.substring(fileName.lastIndexOf('.'))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
""
|
||||
}
|
||||
}
|
||||
@@ -261,7 +259,13 @@ constructor(
|
||||
if (selectedTunnel != null) {
|
||||
saveSettings(uiState.value.settings.copy(defaultTunnel = selectedTunnel.toString()))
|
||||
.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.FlowRow
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -55,7 +53,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -69,6 +66,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
@@ -76,16 +74,19 @@ import com.wireguard.android.backend.Tunnel
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
import com.zaneschepke.wireguardautotunnel.util.FileUtils
|
||||
import com.zaneschepke.wireguardautotunnel.util.Result
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import java.io.File
|
||||
|
||||
@OptIn(
|
||||
@@ -95,15 +96,16 @@ import java.io.File
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
viewModel: SettingsViewModel = hiltViewModel(),
|
||||
padding: PaddingValues,
|
||||
showSnackbarMessage: (String) -> Unit,
|
||||
focusRequester: FocusRequester
|
||||
appViewModel: AppViewModel,
|
||||
navController: NavController,
|
||||
focusRequester: FocusRequester,
|
||||
) {
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val scrollState = rememberScrollState()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val pinExists = remember { mutableStateOf(PinManager.pinExists()) }
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -113,21 +115,14 @@ fun SettingsScreen(
|
||||
var showLocationServicesAlertDialog by remember { mutableStateOf(false) }
|
||||
var didExportFiles by remember { mutableStateOf(false) }
|
||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||
val focusRequester2 = remember { FocusRequester() }
|
||||
|
||||
val screenPadding = 5.dp
|
||||
val fillMaxWidth = .85f
|
||||
|
||||
if (uiState.loading) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val startForResult =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult ->
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val intent = result.data
|
||||
result.data
|
||||
// Handle the Intent
|
||||
}
|
||||
viewModel.setBatteryOptimizeDisableShown()
|
||||
@@ -141,9 +136,10 @@ fun SettingsScreen(
|
||||
}
|
||||
FileUtils.saveFilesToZip(context, files)
|
||||
didExportFiles = true
|
||||
showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||
appViewModel.showSnackbarMessage(Event.Message.ConfigsExported.message)
|
||||
} 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() {
|
||||
if (uiState.isBatteryOptimizeDisableShown || isBatteryOptimizationsDisabled()) {
|
||||
viewModel.toggleAutoTunnel()
|
||||
if (appViewModel.isRequiredPermissionGranted()) {
|
||||
viewModel.toggleAutoTunnel()
|
||||
}
|
||||
} else {
|
||||
requestBatteryOptimizationsDisabled()
|
||||
}
|
||||
@@ -174,7 +172,7 @@ fun SettingsScreen(
|
||||
viewModel.onSaveTrustedSSID(currentText).let {
|
||||
when (it) {
|
||||
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 (
|
||||
WireGuardAutoTunnel.isRunningOnAndroidTv() &&
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
|
||||
) {
|
||||
checkFineLocationGranted()
|
||||
} else {
|
||||
@@ -248,12 +246,16 @@ fun SettingsScreen(
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(padding),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.LocationOff,
|
||||
contentDescription = stringResource(id = R.string.map),
|
||||
modifier = Modifier.padding(30.dp).size(128.dp),
|
||||
modifier = Modifier
|
||||
.padding(30.dp)
|
||||
.size(128.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.prominent_background_location_title),
|
||||
@@ -269,11 +271,15 @@ fun SettingsScreen(
|
||||
)
|
||||
Row(
|
||||
modifier =
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier.fillMaxWidth().padding(10.dp)
|
||||
} else {
|
||||
Modifier.fillMaxWidth().padding(30.dp)
|
||||
},
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(30.dp)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
@@ -301,11 +307,11 @@ fun SettingsScreen(
|
||||
},
|
||||
onError = { _ ->
|
||||
showAuthPrompt = false
|
||||
showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthenticationFailed.message)
|
||||
},
|
||||
onFailure = {
|
||||
showAuthPrompt = false
|
||||
showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
appViewModel.showSnackbarMessage(Event.Error.AuthorizationFailed.message)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -314,7 +320,7 @@ fun SettingsScreen(
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.one_tunnel_required),
|
||||
@@ -329,12 +335,15 @@ fun SettingsScreen(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier.fillMaxSize().verticalScroll(scrollState).clickable(
|
||||
indication = null,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
@@ -342,14 +351,17 @@ fun SettingsScreen(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp)
|
||||
})
|
||||
.padding(bottom = 10.dp),
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 20.dp)
|
||||
})
|
||||
.padding(bottom = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -363,38 +375,43 @@ fun SettingsScreen(
|
||||
ConfigurationToggle(
|
||||
stringResource(id = R.string.tunnel_on_wifi),
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isTunnelOnWifiEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleTunnelOnWifi() },
|
||||
modifier =
|
||||
if (uiState.settings.isAutoTunnelEnabled) Modifier
|
||||
else
|
||||
Modifier.focusRequester(focusRequester).focusProperties {
|
||||
down = focusRequester2
|
||||
},
|
||||
if (uiState.settings.isAutoTunnelEnabled) Modifier
|
||||
else
|
||||
Modifier
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
|
||||
Column {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(screenPadding).fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.padding(screenPadding)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
) {
|
||||
uiState.settings.trustedNetworkSSIDs.forEach { ssid ->
|
||||
ClickableIconButton(
|
||||
onClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
focusRequester.requestFocus()
|
||||
viewModel.onDeleteTrustedSSID(ssid)
|
||||
focusRequester2.requestFocus()
|
||||
}
|
||||
},
|
||||
onIconClick = { viewModel.onDeleteTrustedSSID(ssid) },
|
||||
onIconClick = {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv()) focusRequester.requestFocus()
|
||||
viewModel.onDeleteTrustedSSID(ssid)
|
||||
|
||||
},
|
||||
text = ssid,
|
||||
icon = Icons.Filled.Close,
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
)
|
||||
}
|
||||
if (uiState.settings.trustedNetworkSSIDs.isEmpty()) {
|
||||
@@ -407,24 +424,24 @@ fun SettingsScreen(
|
||||
}
|
||||
OutlinedTextField(
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
value = currentText,
|
||||
onValueChange = { currentText = it },
|
||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||
modifier =
|
||||
Modifier.padding(
|
||||
start = screenPadding,
|
||||
top = 5.dp,
|
||||
bottom = 10.dp,
|
||||
)
|
||||
.focusRequester(focusRequester2),
|
||||
Modifier
|
||||
.padding(
|
||||
start = screenPadding,
|
||||
top = 5.dp,
|
||||
bottom = 10.dp,
|
||||
),
|
||||
maxLines = 1,
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { saveTrustedSSID() }),
|
||||
trailingIcon = {
|
||||
if (currentText != "") {
|
||||
@@ -432,19 +449,19 @@ fun SettingsScreen(
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Add,
|
||||
contentDescription =
|
||||
if (currentText == "") {
|
||||
stringResource(
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_empty_description,
|
||||
)
|
||||
} else {
|
||||
stringResource(
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_value_description,
|
||||
)
|
||||
},
|
||||
if (currentText == "") {
|
||||
stringResource(
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_empty_description,
|
||||
)
|
||||
} else {
|
||||
stringResource(
|
||||
id =
|
||||
R.string
|
||||
.trusted_ssid_value_description,
|
||||
)
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
@@ -456,8 +473,8 @@ fun SettingsScreen(
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.tunnel_mobile_data),
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isTunnelOnMobileDataEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleTunnelOnMobileData() },
|
||||
@@ -465,29 +482,29 @@ fun SettingsScreen(
|
||||
ConfigurationToggle(
|
||||
stringResource(id = R.string.tunnel_on_ethernet),
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isTunnelOnEthernetEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleTunnelOnEthernet() },
|
||||
)
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.battery_saver),
|
||||
stringResource(R.string.restart_on_ping),
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isBatterySaverEnabled,
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled),
|
||||
checked = uiState.settings.isPingEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleBatterySaver() },
|
||||
onCheckChanged = { viewModel.onToggleRestartOnPing() },
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
||||
else
|
||||
Modifier.focusRequester(
|
||||
focusRequester,
|
||||
))
|
||||
(if (!uiState.settings.isAutoTunnelEnabled) Modifier
|
||||
else
|
||||
Modifier.focusRequester(
|
||||
focusRequester,
|
||||
))
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
@@ -497,19 +514,22 @@ fun SettingsScreen(
|
||||
onClick = {
|
||||
if (
|
||||
uiState.settings.isTunnelOnWifiEnabled &&
|
||||
!uiState.settings.isAutoTunnelEnabled
|
||||
!uiState.settings.isAutoTunnelEnabled
|
||||
) {
|
||||
when (false) {
|
||||
isBackgroundLocationGranted ->
|
||||
showSnackbarMessage(
|
||||
Event.Error.BackgroundLocationRequired.message
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Error.BackgroundLocationRequired.message,
|
||||
)
|
||||
|
||||
fineLocationState.status.isGranted ->
|
||||
showSnackbarMessage(
|
||||
Event.Error.PreciseLocationRequired.message
|
||||
appViewModel.showSnackbarMessage(
|
||||
Event.Error.PreciseLocationRequired.message,
|
||||
)
|
||||
|
||||
viewModel.isLocationEnabled(context) ->
|
||||
showLocationServicesAlertDialog = true
|
||||
|
||||
else -> {
|
||||
handleAutoTunnelToggle()
|
||||
}
|
||||
@@ -536,7 +556,9 @@ fun SettingsScreen(
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.fillMaxWidth(fillMaxWidth).padding(vertical = 10.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(vertical = 10.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -550,15 +572,15 @@ fun SettingsScreen(
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.use_kernel),
|
||||
enabled =
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||
!(uiState.settings.isAutoTunnelEnabled ||
|
||||
uiState.settings.isAlwaysOnVpnEnabled ||
|
||||
(uiState.vpnState.status == Tunnel.State.UP)),
|
||||
checked = uiState.settings.isKernelEnabled,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
viewModel.onToggleKernelMode().let {
|
||||
when (it) {
|
||||
is Result.Error -> showSnackbarMessage(it.error.message)
|
||||
is Result.Error -> appViewModel.showSnackbarMessage(it.error.message)
|
||||
is Result.Success -> {}
|
||||
}
|
||||
}
|
||||
@@ -567,26 +589,27 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth(fillMaxWidth)
|
||||
.padding(vertical = 10.dp)
|
||||
.padding(bottom = 140.dp),
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(vertical = 10.dp)
|
||||
.padding(bottom = 140.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier = Modifier.padding(15.dp),
|
||||
) {
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.other),
|
||||
padding = screenPadding,
|
||||
)
|
||||
SectionTitle(
|
||||
title = stringResource(id = R.string.other),
|
||||
padding = screenPadding,
|
||||
)
|
||||
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.always_on_vpn_support),
|
||||
enabled = !uiState.settings.isAutoTunnelEnabled,
|
||||
@@ -601,9 +624,27 @@ fun SettingsScreen(
|
||||
padding = screenPadding,
|
||||
onCheckChanged = { viewModel.onToggleShortcutsEnabled() },
|
||||
)
|
||||
}
|
||||
ConfigurationToggle(
|
||||
stringResource(R.string.enable_app_lock),
|
||||
enabled = true,
|
||||
checked = pinExists.value,
|
||||
padding = screenPadding,
|
||||
onCheckChanged = {
|
||||
if (pinExists.value) {
|
||||
PinManager.clearPin()
|
||||
pinExists.value = PinManager.pinExists()
|
||||
} else {
|
||||
navController.navigate(Screen.Lock.route)
|
||||
}
|
||||
},
|
||||
)
|
||||
if(!WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxSize().padding(top = 5.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = 5.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(
|
||||
@@ -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 vpnState: VpnState = VpnState(),
|
||||
val isLocationDisclosureShown: Boolean = true,
|
||||
val isBatteryOptimizeDisableShown: Boolean = false,
|
||||
val loading: Boolean = true
|
||||
val isBatteryOptimizeDisableShown: Boolean = false
|
||||
)
|
||||
|
||||
@@ -49,7 +49,6 @@ constructor(
|
||||
tunnelState,
|
||||
preferences?.get(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: false,
|
||||
preferences?.get(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: false,
|
||||
false
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
@@ -183,9 +182,10 @@ constructor(
|
||||
if (!uiState.value.settings.isKernelEnabled) {
|
||||
try {
|
||||
rootShell.start()
|
||||
Timber.d("Root shell accepted!")
|
||||
Timber.i("Root shell accepted!")
|
||||
saveKernelMode(on = true)
|
||||
} catch (e: RootShell.RootShellException) {
|
||||
Timber.e(e)
|
||||
saveKernelMode(on = false)
|
||||
return Result.Error(Event.Error.RootDenied)
|
||||
}
|
||||
@@ -194,4 +194,12 @@ constructor(
|
||||
}
|
||||
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
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.Intent.createChooser
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -20,10 +16,11 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ArrowForward
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowForward
|
||||
import androidx.compose.material.icons.rounded.Book
|
||||
import androidx.compose.material.icons.rounded.FormatListNumbered
|
||||
import androidx.compose.material.icons.rounded.Mail
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.zaneschepke.wireguardautotunnel.BuildConfig
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Screen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.screen.LoadingScreen
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.Event
|
||||
|
||||
@Composable
|
||||
fun SupportScreen(
|
||||
viewModel: SupportViewModel = hiltViewModel(),
|
||||
padding: PaddingValues,
|
||||
showSnackbarMessage: (String) -> Unit,
|
||||
appViewModel: AppViewModel,
|
||||
navController: NavController,
|
||||
focusRequester: FocusRequester
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -67,47 +64,14 @@ fun SupportScreen(
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
fun openWebPage(url: String) {
|
||||
try {
|
||||
val webpage: Uri = Uri.parse(url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||
}
|
||||
}
|
||||
|
||||
fun launchEmail() {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
type = Constants.EMAIL_MIME_TYPE
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email)))
|
||||
putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject))
|
||||
}
|
||||
startActivity(
|
||||
context,
|
||||
createChooser(intent, context.getString(R.string.email_chooser)),
|
||||
null,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
showSnackbarMessage(Event.Error.Exception(e).message)
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.loading) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.focusable()
|
||||
.padding(padding),
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.focusable()
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
@@ -116,15 +80,19 @@ fun SupportScreen(
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier =
|
||||
(if (WireGuardAutoTunnel.isRunningOnAndroidTv()) {
|
||||
Modifier.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 10.dp)
|
||||
} else {
|
||||
Modifier.fillMaxWidth(fillMaxWidth).padding(top = 20.dp)
|
||||
Modifier
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
.padding(top = 20.dp)
|
||||
})
|
||||
.padding(bottom = 25.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(20.dp)) {
|
||||
val forwardIcon = Icons.AutoMirrored.Rounded.ArrowForward
|
||||
Text(
|
||||
stringResource(R.string.thank_you),
|
||||
textAlign = TextAlign.Start,
|
||||
@@ -139,8 +107,10 @@ fun SupportScreen(
|
||||
modifier = Modifier.padding(bottom = 20.dp),
|
||||
)
|
||||
TextButton(
|
||||
onClick = { openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||
modifier = Modifier.padding(vertical = 5.dp).focusRequester(focusRequester),
|
||||
onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) },
|
||||
modifier = Modifier
|
||||
.padding(vertical = 5.dp)
|
||||
.focusRequester(focusRequester),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -148,19 +118,26 @@ fun SupportScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
Icon(Icons.Rounded.Book, stringResource(id = R.string.docs))
|
||||
val icon = Icons.Rounded.Book
|
||||
Icon(icon, icon.name)
|
||||
Text(
|
||||
stringResource(id = R.string.docs_description),
|
||||
textAlign = TextAlign.Justify,
|
||||
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(
|
||||
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),
|
||||
) {
|
||||
Row(
|
||||
@@ -169,9 +146,10 @@ fun SupportScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
val icon = ImageVector.vectorResource(R.drawable.discord)
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.discord),
|
||||
stringResource(id = R.string.discord),
|
||||
icon,
|
||||
icon.name,
|
||||
Modifier.size(25.dp),
|
||||
)
|
||||
Text(
|
||||
@@ -180,12 +158,18 @@ fun SupportScreen(
|
||||
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(
|
||||
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),
|
||||
) {
|
||||
Row(
|
||||
@@ -194,23 +178,30 @@ fun SupportScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
val icon = ImageVector.vectorResource(R.drawable.github)
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.github),
|
||||
stringResource(id = R.string.github),
|
||||
imageVector = icon,
|
||||
icon.name,
|
||||
Modifier.size(25.dp),
|
||||
)
|
||||
Text(
|
||||
"Open an issue",
|
||||
stringResource(id = R.string.open_issue),
|
||||
textAlign = TextAlign.Justify,
|
||||
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(
|
||||
onClick = { launchEmail() },
|
||||
onClick = { appViewModel.launchEmail() },
|
||||
modifier = Modifier.padding(vertical = 5.dp),
|
||||
) {
|
||||
Row(
|
||||
@@ -219,14 +210,48 @@ fun SupportScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row {
|
||||
Icon(Icons.Rounded.Mail, stringResource(id = R.string.email))
|
||||
val icon = Icons.Rounded.Mail
|
||||
Icon(icon, icon.name)
|
||||
Text(
|
||||
stringResource(id = R.string.email_description),
|
||||
textAlign = TextAlign.Justify,
|
||||
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,
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||
appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url))
|
||||
},
|
||||
)
|
||||
Row(
|
||||
|
||||
@@ -2,4 +2,4 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.Settings
|
||||
|
||||
data class SupportUiState(val settings: Settings = Settings(), 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 =
|
||||
settingsRepository
|
||||
.getSettingsFlow()
|
||||
.map { SupportUiState(it, false) }
|
||||
.map { SupportUiState(it) }
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
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()
|
||||
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
|
||||
!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
|
||||
|
||||
object Constants {
|
||||
|
||||
const val BASE_LOG_FILE_NAME = "wgtunnel-logs"
|
||||
const val LOG_BUFFER_SIZE = 3_000L
|
||||
|
||||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1000L // 10 minutes
|
||||
const val DEFAULT_WATCHER_WAKE_LOCK_TIMEOUT = 30 * 60 * 1000L // 30 minutes
|
||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
||||
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3000L
|
||||
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
|
||||
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
|
||||
const val VPN_CONNECTED_NOTIFICATION_DELAY = 3_000L
|
||||
const val TOGGLE_TUNNEL_DELAY = 300L
|
||||
const val CONF_FILE_EXTENSION = ".conf"
|
||||
const val ZIP_FILE_EXTENSION = ".zip"
|
||||
const val URI_CONTENT_SCHEME = "content"
|
||||
const val URI_PACKAGE_SCHEME = "package"
|
||||
const val ALLOWED_FILE_TYPES = "*/*"
|
||||
const val TEXT_MIME_TYPE = "text/plain"
|
||||
const val GOOGLE_TV_EXPLORER_STUB = "com.google.android.tv.frameworkpackagestubs"
|
||||
const val ANDROID_TV_EXPLORER_STUB = "com.android.tv.frameworkpackagestubs"
|
||||
const val EMAIL_MIME_TYPE = "message/rfc822"
|
||||
@@ -19,4 +22,11 @@ object Constants {
|
||||
|
||||
const val SUBSCRIPTION_TIMEOUT = 5_000L
|
||||
const val FOCUS_REQUEST_DELAY = 500L
|
||||
|
||||
const val BACKUP_PING_HOST = "1.1.1.1"
|
||||
const val PING_TIMEOUT = 5_000L
|
||||
const val VPN_RESTART_DELAY = 1_000L
|
||||
const val PING_INTERVAL = 60_000L
|
||||
const val PING_COOLDOWN = PING_INTERVAL * 60 //one hour
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,12 @@ sealed class Event {
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_ssid_exists)
|
||||
}
|
||||
|
||||
data class ConfigParseError(val appendedMessage : String) : Error() {
|
||||
override val message: String =
|
||||
WireGuardAutoTunnel.instance.getString(R.string.config_parse_error) + (
|
||||
if (appendedMessage != "") ": ${appendedMessage.trim()}" else "")
|
||||
}
|
||||
|
||||
data object RootDenied : Error() {
|
||||
override val message: String
|
||||
get() = WireGuardAutoTunnel.instance.getString(R.string.error_root_denied)
|
||||
|
||||
@@ -31,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 {
|
||||
val df = DecimalFormat("#.###")
|
||||
return df.format(this)
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.MediaStore.MediaColumns
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.util.zip.ZipEntry
|
||||
@@ -43,6 +44,31 @@ object FileUtils {
|
||||
return null
|
||||
}
|
||||
|
||||
fun saveFileToDownloads(context: Context, content: String, fileName: String) {
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(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>) {
|
||||
val zipOutputStream =
|
||||
createDownloadsFileOutputStream(
|
||||
|
||||
@@ -19,7 +19,15 @@ object NumberUtils {
|
||||
}
|
||||
|
||||
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? {
|
||||
|
||||
@@ -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"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
android:viewportWidth="640"
|
||||
android:viewportHeight="640">
|
||||
<group android:scaleX="0.6"
|
||||
android:scaleY="0.6"
|
||||
android:translateX="128"
|
||||
android:translateY="128">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
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>
|
||||
|
||||
@@ -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"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_banner_background" />
|
||||
<foreground android:drawable="@mipmap/ic_banner_foreground" />
|
||||
<background android:drawable="@color/ic_banner_background"/>
|
||||
<foreground android:drawable="@drawable/ic_banner_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<!-- support for adaptive theme icons -->
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<!-- support for adaptive theme icons -->
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</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"?>
|
||||
<resources>
|
||||
<color name="ic_banner_background">#121212</color>
|
||||
<color name="ic_banner_background">#1D1A20</color>
|
||||
</resources>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#1C1B20</color>
|
||||
<color name="ic_launcher_background">#1D1A20</color>
|
||||
</resources>
|
||||
@@ -6,21 +6,21 @@
|
||||
<string name="watcher_channel_id">Watcher Channel</string>
|
||||
<string name="watcher_channel_name">Watcher Notification Channel</string>
|
||||
<string name="foreground_file">FOREGROUND_FILE</string>
|
||||
<string name="github_url">https://github.com/zaneschepke/wgtunnel/issues</string>
|
||||
<string name="docs_url">https://zaneschepke.com/wgtunnel-docs/overview.html</string>
|
||||
<string name="privacy_policy_url">https://zaneschepke.com/wgtunnel-docs/privacypolicy.html</string>
|
||||
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
|
||||
<string name="docs_url" translatable="false">https://zaneschepke.com/wgtunnel-docs/overview.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="turn_off_tunnel">Action requires tunnel off</string>
|
||||
<string name="no_tunnels">No tunnels added yet!</string>
|
||||
<string name="tunnel_exists">Tunnel name already exists</string>
|
||||
<string name="discord_url">https://discord.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_text_active">Monitoring network state changes: active</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_text">Connected to tunnel -</string>
|
||||
<string name="vpn_permission_required">VPN permission is required for the app to work properly. If this permission is not launching, please disable \"Always-on VPN\" in your phone settings for the official WireGuard mobile app and try again.</string>
|
||||
<string name="notification_permission_required">Notifications permission is required for the app to work properly.</string>
|
||||
<string name="notification_permission_required">Notifications permission required.</string>
|
||||
<string name="open_settings">Open Settings</string>
|
||||
<string name="add_trusted_ssid">Add trusted wifi name</string>
|
||||
<string name="tunnels">Tunnels</string>
|
||||
@@ -39,15 +39,13 @@
|
||||
<string name="thank_you">Thank you for using WG Tunnel!</string>
|
||||
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
||||
<string name="trusted_ssid_value_description">Submit SSID</string>
|
||||
<string name="config_validation">[Interface]</string>
|
||||
<string name="config_validation" translatable="false">[Interface]</string>
|
||||
<string name="add_tunnels_text">Add from file or zip</string>
|
||||
<string name="open_file">File Open</string>
|
||||
<string name="add_from_qr">Add from QR code</string>
|
||||
<string name="qr_scan">QR Scan</string>
|
||||
<string name="tunnel_edit">Tunnel Edit</string>
|
||||
<string name="tunnel_name">Tunnel Name</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="add_tunnel">Add Tunnel</string>
|
||||
<string name="exclude">Exclude</string>
|
||||
<string name="include">Include</string>
|
||||
@@ -73,7 +71,7 @@
|
||||
<string name="last_handshake">Last handshake</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="restart">Restart Tunnel</string>
|
||||
<string name="vpn_connection_failed">VPN Connection Failed</string>
|
||||
<string name="vpn_connection_failed">Connection failed</string>
|
||||
<string name="failed_connection_to">Failed connection to -</string>
|
||||
<string name="initial_connection_failure_message">Attempting to connect to server after 30 seconds of no response.</string>
|
||||
<string name="lost_connection_failure_message">Attempting to reconnect to server after more than one minute of no response.</string>
|
||||
@@ -90,7 +88,7 @@
|
||||
<string name="clear_icon">Clear Icon</string>
|
||||
<string name="search_icon">Search Icon</string>
|
||||
<string name="attempt_connection">Attempting connection..</string>
|
||||
<string name="vpn_starting">VPN Starting</string>
|
||||
<string name="vpn_starting">VPN starting</string>
|
||||
<string name="db_name">wg-tunnel-db</string>
|
||||
<string name="scanning_qr">Scanning for QR</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_off">Primary VPN off</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_on_tunnel">Action requires active tunnel</string>
|
||||
<string name="add_peer">Add peer</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="done">Done</string>
|
||||
<string name="interface_">Interface</string>
|
||||
<string name="rotate_keys">Rotate keys</string>
|
||||
@@ -119,7 +115,6 @@
|
||||
<string name="comma_separated_list">comma separated list</string>
|
||||
<string name="listen_port">Listen port</string>
|
||||
<string name="random">(random)</string>
|
||||
<string name="auto">(auto)</string>
|
||||
<string name="optional">(optional)</string>
|
||||
<string name="optional_no_recommend">(optional, not recommended)</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="status">status</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_chooser">Send an email…</string>
|
||||
<string name="go">go</string>
|
||||
<string name="docs_description">Read the docs (WIP)</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="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>
|
||||
@@ -167,4 +158,26 @@
|
||||
<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="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>
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.WireguardAutoTunnel" parent="@style/Theme.AppCompat.NoActionBar">
|
||||
<item name="android:windowBackground">@color/black_background</item>
|
||||
</style>
|
||||
|
||||
|
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 {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.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.androidLibrary) apply false
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import org.gradle.api.invocation.Gradle
|
||||
import java.io.File
|
||||
|
||||
object BuildHelper {
|
||||
private fun getCurrentFlavor(gradle: Gradle): String {
|
||||
@@ -21,6 +22,17 @@ object BuildHelper {
|
||||
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 {
|
||||
return getCurrentFlavor(gradle) == "general"
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
object Constants {
|
||||
const val VERSION_NAME = "3.3.3"
|
||||
const val VERSION_NAME = "3.3.9"
|
||||
const val JVM_TARGET = "17"
|
||||
const val VERSION_CODE = 33300
|
||||
const val VERSION_CODE = 33900
|
||||
const val TARGET_SDK = 34
|
||||
const val MIN_SDK = 26
|
||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||
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"
|
||||
|
||||
@@ -2,6 +2,18 @@ default_platform(:android)
|
||||
|
||||
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"
|
||||
lane :beta do
|
||||
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
|
||||