Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 8b94c951f3 chore(deps): bump ktorClientCore from 3.1.1 to 3.1.2
Bumps `ktorClientCore` from 3.1.1 to 3.1.2.

Updates `io.ktor:ktor-client-cio` from 3.1.1 to 3.1.2
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.1.1...3.1.2)

Updates `io.ktor:ktor-client-content-negotiation` from 3.1.1 to 3.1.2
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.1.1...3.1.2)

Updates `io.ktor:ktor-client-core` from 3.1.1 to 3.1.2
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.1.1...3.1.2)

Updates `io.ktor:ktor-client-okhttp` from 3.1.1 to 3.1.2
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.1.1...3.1.2)

Updates `io.ktor:ktor-serialization-kotlinx-json` from 3.1.1 to 3.1.2
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.1.1...3.1.2)

---
updated-dependencies:
- dependency-name: io.ktor:ktor-client-cio
  dependency-version: 3.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.ktor:ktor-client-content-negotiation
  dependency-version: 3.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.ktor:ktor-client-core
  dependency-version: 3.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.ktor:ktor-client-okhttp
  dependency-version: 3.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: io.ktor:ktor-serialization-kotlinx-json
  dependency-version: 3.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-22 13:51:28 +00:00
72 changed files with 751 additions and 960 deletions
+42 -45
View File
@@ -1,5 +1,4 @@
name: Build
name: build
on:
workflow_dispatch:
inputs:
@@ -13,14 +12,6 @@ on:
- prerelease
- nightly
- release
flavor:
type: choice
description: "Product flavor"
required: true
default: fdroid
options:
- fdroid
- full
secrets:
SIGNING_KEY_ALIAS:
required: false
@@ -39,11 +30,6 @@ on:
description: "Build type"
required: true
default: debug
flavor:
type: string
description: "Product flavor"
required: false
default: fdroid
secrets:
SIGNING_KEY_ALIAS:
required: false
@@ -55,7 +41,6 @@ on:
required: false
KEYSTORE:
required: false
env:
UPLOAD_DIR_ANDROID: android_artifacts
@@ -63,17 +48,15 @@ jobs:
build:
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 }}
SIGNING_KEY_ALIAS: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.ANDROID_SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
outputs:
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
@@ -82,47 +65,61 @@ jobs:
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 }}
encodedString: ${{ secrets.ANDROID_KEYSTORE }}
# create keystore path for gradle to read
- name: Create keystore path env var
if: ${{ inputs.build_type != 'debug' }}
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Create service_account.json
if: ${{ inputs.build_type != 'debug' }}
id: createServiceAccount
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
- name: Build APK
run: echo '${{ secrets.ANDROID_SERVICE_ACCOUNT_JSON }}' > service_account.json
- name: Build Fdroid Release APK
if: ${{ inputs.build_type == 'release' }}
run: ./gradlew :app:assembleFdroidRelease --info
- name: Build Fdroid Prerelease APK
if: ${{ inputs.build_type == 'prerelease' }}
run: ./gradlew :app:assembleFdroidPrerelease --info
- name: Build Fdroid Nightly APK
if: ${{ inputs.build_type == 'nightly' }}
run: ./gradlew :app:assembleFdroidNightly --info
- name: Build Debug APK
if: ${{ inputs.build_type == 'debug' }}
run: ./gradlew :app:assembleFdroidDebug --stacktrace
# bump versionCode for nightly and prerelease builds
- name: Commit and push versionCode changes
if: ${{ inputs.build_type == 'nightly' || inputs.build_type == 'prerelease' }}
run: |
flavor=${{ inputs.flavor }}
build_type=${{ inputs.build_type }}
case $build_type in
"release")
./gradlew :app:assemble${flavor^}Release --info
;;
"prerelease")
./gradlew :app:assemble${flavor^}Prerelease --info
;;
"nightly")
./gradlew :app:assemble${flavor^}Nightly --info
;;
"debug")
./gradlew :app:assemble${flavor^}Debug --stacktrace
;;
esac
git config --global user.name 'GitHub Actions'
git config --global user.email 'actions@github.com'
git add versionCode.txt
git commit -m "Automated build update"
- name: Get release apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload APK
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload release apk
uses: actions/upload-artifact@v4
with:
name: android_artifacts_${{ inputs.flavor }}
path: app/build/outputs/apk/${{ inputs.flavor }}/release/wgtunnel-${{ inputs.flavor }}-release-*.apk
name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{github.workspace}}/${{ steps.apk-path.outputs.path }}
retention-days: 1
if-no-files-found: warn
+69 -79
View File
@@ -2,12 +2,12 @@ name: publish
on:
schedule:
- cron: "4 3 * * *"
- cron: "4 3 * * *"
workflow_dispatch:
inputs:
track:
type: choice
description: "Google Play release track"
description: "Google play release track"
options:
- none
- internal
@@ -30,28 +30,12 @@ on:
description: "Tag name for release"
required: false
default: nightly
flavor:
type: choice
description: "Product flavor"
required: true
default: full
options:
- fdroid
- full
workflow_call:
inputs:
flavor:
type: string
description: "Product flavor"
required: false
default: full
env:
UPLOAD_DIR_ANDROID: android_artifacts
permissions:
contents: write
packages: write
jobs:
check_commits:
@@ -59,73 +43,66 @@ jobs:
runs-on: ubuntu-latest
outputs:
has_new_commits: ${{ steps.check.outputs.new_commits }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 0 # This fetches all history so we can check commits
- name: Check for new commits
id: check
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
# This script checks for commits newer than 23 hours ago
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
build-fdroid:
if: ${{ inputs.release_type == 'release' || inputs.flavor == 'fdroid' }}
build:
if: ${{ inputs.release_type != 'none' }}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }}
flavor: fdroid
build-full:
if: ${{ inputs.release_type == 'release' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' || inputs.flavor == 'full' }}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }}
flavor: full
publish:
needs:
- check_commits
- build-full
- build
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: publish-github
runs-on: ubuntu-latest
env:
GH_USER: ${{ secrets.PAT_USERNAME }}
# GH needed for gh cli
GH_TOKEN: ${{ secrets.PAT }}
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Install system dependencies
run: |
sudo apt update && sudo apt install -y gh apksigner
- name: Set TAG_NAME
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV
elif [ "${{ github.event_name }}" = "schedule" ]; then
echo "TAG_NAME=nightly" >> $GITHUB_ENV
echo "RELEASE_TYPE=nightly" >> $GITHUB_ENV
fi
# update latest tag
- name: Set latest tag
uses: rickstaa/action-create-tag@v1
id: tag_creation
with:
tag: "latest"
tag: "latest" # or any tag name you wish to use
message: "Automated tag for HEAD commit"
force_push_tag: true
github_token: ${{ secrets.GITHUB_TOKEN }}
tag_exists_error: false
- name: Get latest release
id: latest_release
uses: kaliber5/action-get-release@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
latest: true
- name: Generate Changelog
id: changelog
uses: requarks/changelog-action@v1
@@ -133,20 +110,40 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
toTag: ${{ github.event_name == 'schedule' && 'nightly' || steps.latest_release.outputs.tag_name }}
fromTag: "latest"
writeToFile: false
writeToFile: false # we won't write to file, just output
- name: Get version code
if: ${{ inputs.release_type == 'release' }}
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
- name: Push changes
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' }}
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.PAT }}
branch: ${{ github.ref }}
- name: Make download dir
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v4
with:
pattern: android_artifacts_*
name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{ github.workspace }}/temp
merge-multiple: true
# Setup TAG_NAME, which is used as a general "name"
- if: github.event_name == 'workflow_dispatch'
run: echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV
- if: github.event_name == 'schedule'
run: echo "TAG_NAME=nightly" >> $GITHUB_ENV
- name: Set version release notes
if: ${{ inputs.release_type == 'release' }}
run: |
VERSION_NAME=$(grep "const val VERSION_NAME" buildSrc/src/main/kotlin/Constants.kt | awk -F'"' '{print $2}')
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${VERSION_NAME}.txt || echo "No changelog found for ${VERSION_NAME}")"
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt)"
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$RELEASE_NOTES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
@@ -155,40 +152,32 @@ jobs:
if: ${{ contains(env.TAG_NAME, 'nightly') }}
run: |
echo "RELEASE_NOTES=Nightly build for the latest development version of the app." >> $GITHUB_ENV
gh release delete nightly --yes || true
git push origin :nightly || true
- name: On prerelease release notes
if: ${{ inputs.release_type == 'prerelease' }}
run: |
echo "RELEASE_NOTES=Testing version of app for specific feature." >> $GITHUB_ENV
gh release delete ${{ github.event.inputs.tag_name }} --yes || true
- name: Delete previous release
if: ${{ contains(env.TAG_NAME, 'nightly') || inputs.release_type == 'prerelease' }}
uses: ClementTsang/delete-tag-and-release@v0.4.0
with:
tag_name: ${{ env.TAG_NAME }}
delete_release: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get checksums
- name: Get checksum
id: checksum
run: |
checksums=""
for file_path in $(find ${{ github.workspace }}/temp -type f -iname "*.apk"); do
checksum=$(apksigner verify -print-certs $file_path | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")
checksums="$checksums\n$file_path: $checksum"
done
echo "checksum<<EOF" >> $GITHUB_OUTPUT
echo -e "$checksums" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Release
file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | tail -n1)
echo "checksum=$(apksigner verify -print-certs $file_path | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
- name: Create Release with Fastlane changelog notes
id: create_release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body: |
${{ env.RELEASE_NOTES }}
SHA-256 fingerprints for the 4096-bit signing certificate:
SHA-256 fingerprint for the 4096-bit signing certificate:
```sh
${{ steps.checksum.outputs.checksum }}
```
@@ -207,20 +196,18 @@ jobs:
make_latest: ${{ inputs.release_type == 'release' }}
files: |
${{ github.workspace }}/temp/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-fdroid-public:
publish-fdroid:
runs-on: ubuntu-latest
needs:
- build-fdroid
- build
if: inputs.release_type == 'release'
steps:
- name: Dispatch update for fdroid repo
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: wgtunnel/fdroid
token: ${{ secrets.PAT }}
repository: zaneschepke/fdroid
event-type: fdroid-update
publish-play:
@@ -229,11 +216,13 @@ jobs:
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 }}
SIGNING_KEY_ALIAS: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.ANDROID_SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
GH_USER: ${{ secrets.PAT_USERNAME }}
GH_TOKEN: ${{ secrets.PAT }}
steps:
- uses: actions/checkout@v4
@@ -255,7 +244,7 @@ jobs:
with:
fileName: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
encodedString: ${{ secrets.ANDROID_KEYSTORE }}
# create keystore path for gradle to read
- name: Create keystore path env var
@@ -274,4 +263,5 @@ jobs:
bundler-cache: true
- name: Distribute app to Prod track 🚀
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane ${{ inputs.track }})
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane ${{ inputs.track }})
+1 -1
View File
@@ -70,5 +70,5 @@ lint/tmp/
app/release/output.json
.idea/codeStyles/
# where we keep our signing secrets locally
app/signing.properties
/.kotlin/
/app/keystore/
+95 -38
View File
@@ -9,14 +9,33 @@ plugins {
alias(libs.plugins.licensee)
}
val versionFile = file("$rootDir/versionCode.txt")
val versionCodeIncrement =
with(getBuildTaskName().lowercase()) {
when {
this.contains(Constants.NIGHTLY) || this.contains(Constants.PRERELEASE) -> {
if (versionFile.exists()) {
versionFile.readText().trim().toInt() + 1
} else {
1
}
}
else -> 0
}
}
android {
namespace = Constants.APP_ID
compileSdk = Constants.TARGET_SDK
androidResources { generateLocaleConfig = true }
// reproducibility
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
@@ -24,12 +43,14 @@ android {
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
targetSdk = Constants.TARGET_SDK
versionCode = computeVersionCode()
versionName = computeVersionName()
versionCode = Constants.VERSION_CODE + versionCodeIncrement
versionName = determineVersionName()
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
sourceSets { getByName("debug").assets.srcDirs(files("$projectDir/schemas")) }
sourceSets {
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
}
buildConfigField(
"String[]",
@@ -43,18 +64,15 @@ android {
signingConfigs {
create(Constants.RELEASE) {
storeFile = file(System.getenv("KEY_STORE_PATH") ?: "keystore/android_keystore.jks")
storePassword =
LocalProperties.get("SIGNING_STORE_PASSWORD")
?: System.getenv("SIGNING_STORE_PASSWORD")
keyAlias =
LocalProperties.get("SIGNING_KEY_ALIAS") ?: System.getenv("SIGNING_KEY_ALIAS")
keyPassword =
LocalProperties.get("SIGNING_KEY_PASSWORD") ?: System.getenv("SIGNING_KEY_PASSWORD")
storeFile = getStoreFile()
storePassword = getSigningProperty(Constants.STORE_PASS_VAR)
keyAlias = getSigningProperty(Constants.KEY_ALIAS_VAR)
keyPassword = getSigningProperty(Constants.KEY_PASS_VAR)
}
}
buildTypes {
// don't strip
packaging.jniLibs.keepDebugSymbols.addAll(
listOf("libwg-go.so", "libwg-quick.so", "libwg.so")
)
@@ -70,7 +88,6 @@ android {
signingConfig = signingConfigs.getByName(Constants.RELEASE)
resValue("string", "provider", "\"${Constants.APP_NAME}.provider\"")
}
debug {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "WG Tunnel - Debug")
@@ -91,21 +108,27 @@ android {
resValue("string", "app_name", "WG Tunnel - Nightly")
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"")
}
}
flavorDimensions.add("type")
applicationVariants.all {
val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
val outputFileName =
"${Constants.APP_NAME}-${variant.flavorName}-" +
"${variant.buildType.name}-${variant.versionName}.apk"
output.outputFileName = outputFileName
}
}
}
flavorDimensions.add(Constants.TYPE)
productFlavors {
create("fdroid") {
dimension = "type"
buildConfigField("String", "FLAVOR", "\"fdroid\"")
dimension = Constants.TYPE
proguardFile("fdroid-rules.pro")
}
create("google") {
dimension = "type"
buildConfigField("String", "FLAVOR", "\"google\"")
}
create("full") { dimension = "type" }
create("general") { dimension = Constants.TYPE }
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
@@ -121,22 +144,9 @@ android {
licensee {
Constants.allowedLicenses.forEach { allow(it) }
allowUrl(Constants.XZING_LICENSE_URL)
allowUrl("https://rafaellins.mit-license.org/2021/")
}
applicationVariants.all {
val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
val outputFileName =
if (variant.flavorName == "fdroid" && variant.buildType.name == "release") {
"${Constants.APP_NAME}-fdroid-release-${variant.versionName}.apk"
} else {
"${Constants.APP_NAME}-${variant.flavorName}-v${variant.versionName}.apk"
}
output.outputFileName = outputFileName
}
// Fix for qrcode-kotlin (MIT, custom URL)
allowUrl("https://rafaellins.mit-license.org/2021/")
}
}
@@ -146,6 +156,8 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
// helpers for implementing LifecycleOwner in a Service
implementation(libs.androidx.lifecycle.service)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
@@ -157,6 +169,7 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.storage)
// test
testImplementation(libs.junit)
testImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.junit)
@@ -167,63 +180,107 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest)
// tunnel
implementation(libs.tunnel)
implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
// logging
implementation(libs.timber)
// compose navigation
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose)
// hilt
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
// accompanist
implementation(libs.accompanist.permissions)
implementation(libs.accompanist.drawablepainter)
// storage
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.datastore.preferences)
// lifecycle
implementation(libs.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.process)
// serialization
implementation(libs.kotlinx.serialization.json)
// ui
implementation(libs.zxing.android.embedded)
implementation(libs.material.icons.extended)
// bio
implementation(libs.androidx.biometric.ktx)
implementation(libs.pin.lock.compose)
// shortcuts
implementation(libs.androidx.core)
// splash
implementation(libs.androidx.core.splashscreen)
// worker
implementation(libs.androidx.work.runtime)
implementation(libs.androidx.hilt.work)
// util
implementation(libs.qrcode.kotlin)
implementation(libs.semver4j)
// Ktor
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.slf4j.android)
}
fun determineVersionName(): String {
return with(getBuildTaskName().lowercase()) {
when {
contains(Constants.NIGHTLY) || contains(Constants.PRERELEASE) ->
Constants.VERSION_NAME + "-${grgitService.service.get().grgit.head().abbreviatedId}"
else -> Constants.VERSION_NAME
}
}
}
val incrementVersionCode by
tasks.registering {
doLast {
val versionFile = file("$rootDir/versionCode.txt")
if (versionFile.exists()) {
versionFile.writeText(versionCodeIncrement.toString())
println("Incremented versionCode to $versionCodeIncrement")
}
}
}
tasks.whenTaskAdded {
if (name.startsWith("assemble") && !name.lowercase().contains("debug")) {
dependsOn(incrementVersionCode)
}
}
tasks.register<Copy>("copyLicenseeJsonToAssets") {
dependsOn("licensee")
val outputAssets = layout.projectDirectory.dir("src/main/assets")
from(layout.buildDirectory.file("reports/licensee/androidFdroidRelease/artifacts.json")) {
rename("artifacts.json", "licenses.json")
}
into(outputAssets)
}
View File
-5
View File
@@ -1,5 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions specific to full -->
<!--updater-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
</manifest>
+8 -2
View File
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!--updater-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@@ -52,7 +54,7 @@
<application
android:name=".WireGuardAutoTunnel"
android:allowBackup="false"
android:banner="@mipmap/ic_banner"
android:banner="@drawable/ic_banner"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
@@ -65,6 +67,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:banner="@mipmap/ic_banner"
android:windowSoftInputMode="adjustNothing"
android:theme="@style/Theme.WireguardAutoTunnel"
android:configChanges="orientation|screenSize|keyboardHidden"
@@ -166,12 +169,15 @@
<receiver
android:name=".core.broadcast.RestartReceiver"
android:enabled="true"
android:exported="false">
android:exported="false"
android:directBootAware="true">
<intent-filter>
<action android:name="android.intent.action.SCREEN_ON" />
<action android:name="android.intent.action.USER_PRESENT" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
@@ -39,6 +39,7 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
@@ -109,7 +110,6 @@ class MainActivity : AppCompatActivity() {
val isTv = isRunningOnTv()
val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
val appViewState by viewModel.appViewState.collectAsStateWithLifecycle()
val tunnelError by viewModel.tunnelManager.errorEvents.collectAsStateWithLifecycle(null)
val navController = rememberNavController()
val backStackEntry by navController.currentBackStackEntryAsState()
@@ -134,7 +134,6 @@ class MainActivity : AppCompatActivity() {
vpnPermissionDenied = true
} else {
vpnPermissionDenied = false
showVpnPermissionDialog = false
}
},
)
@@ -152,15 +151,6 @@ class MainActivity : AppCompatActivity() {
viewModel.handleEvent(AppEvent.SetBatteryOptimizeDisableShown)
}
LaunchedEffect(tunnelError) {
if (tunnelError == null) return@LaunchedEffect
val message = tunnelError!!.second.toStringRes()
val context = this@MainActivity
snackbar.showSnackbar(
context.getString(R.string.tunnel_error_template, context.getString(message))
)
}
with(appViewState) {
LaunchedEffect(isConfigChanged) {
if (isConfigChanged) {
@@ -176,6 +166,21 @@ class MainActivity : AppCompatActivity() {
viewModel.handleEvent(AppEvent.MessageShown)
}
}
LaunchedEffect(appUiState.activeTunnels) {
appUiState.activeTunnels.mapNotNull { (tunnelConf, tunnelState) ->
(tunnelState.status as? TunnelStatus.Error)?.let { error ->
val message = error.error.toStringRes()
val context = this@MainActivity
snackbar.showSnackbar(
context.getString(
R.string.tunnel_error_template,
context.getString(message),
)
)
viewModel.handleEvent(AppEvent.ClearTunnelError(tunnelConf))
}
}
}
LaunchedEffect(popBackStack) {
if (popBackStack) {
navController.popBackStack()
@@ -209,10 +214,7 @@ class MainActivity : AppCompatActivity() {
WireguardAutoTunnelTheme(theme = appUiState.appState.theme) {
VpnDeniedDialog(
showVpnPermissionDialog,
onDismiss = {
showVpnPermissionDialog = false
vpnPermissionDenied = false
},
onDismiss = { showVpnPermissionDialog = false },
)
Scaffold(
@@ -32,15 +32,14 @@ class RestartReceiver : BroadcastReceiver() {
Timber.d("RestartReceiver triggered with action: ${intent.action}")
// screen on for Android TV only to help with sleep shutdowns
val isTv = context.isRunningOnTv()
if (intent.action == Intent.ACTION_SCREEN_ON && !isTv) return
if (intent.action == Intent.ACTION_USER_PRESENT && !isTv) return
serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile()
applicationScope.launch(ioDispatcher) {
val settings = appDataRepository.settings.get()
if (settings.isRestoreOnBootEnabled) {
if (
settings.isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null
) {
if (settings.isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) {
Timber.d("Starting auto-tunnel on boot/update")
serviceManager.startAutoTunnel()
} else {
@@ -1,11 +1,10 @@
package com.zaneschepke.wireguardautotunnel.core.service
import android.content.ComponentName
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.VpnService
import android.os.IBinder
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
@@ -14,6 +13,7 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
@@ -37,37 +37,23 @@ constructor(
private val autoTunnelMutex = Mutex()
private val _tunnelService = MutableStateFlow<TunnelForegroundService?>(null)
private val _autoTunnelService = MutableStateFlow<AutoTunnelService?>(null)
val autoTunnelService = _autoTunnelService.asStateFlow()
private val _autoTunnelActive = MutableStateFlow(false)
val autoTunnelActive = _autoTunnelActive.asStateFlow()
private val tunnelServiceConnection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as? TunnelForegroundService.LocalBinder
_tunnelService.value = binder?.service
Timber.d("TunnelForegroundService connected")
}
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelForegroundService>()
override fun onServiceDisconnected(name: ComponentName) {
_tunnelService.value = null
Timber.d("TunnelForegroundService disconnected")
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
runCatching {
val intent = Intent(context, cls)
if (background) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
}
private val autoTunnelServiceConnection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as? AutoTunnelService.LocalBinder
_autoTunnelService.value = binder?.service
Timber.d("AutoTunnelService connected")
}
override fun onServiceDisconnected(name: ComponentName) {
_autoTunnelService.value = null
Timber.d("AutoTunnelService disconnected")
}
}
.onFailure { Timber.e(it) }
}
fun hasVpnPermission(): Boolean {
return VpnService.prepare(context) == null
@@ -77,13 +63,20 @@ constructor(
autoTunnelMutex.withLock {
val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
if (_autoTunnelService.value != null) return
withContext(ioDispatcher) {
val intent = Intent(context, AutoTunnelService::class.java)
context.startForegroundService(intent)
context.bindService(intent, autoTunnelServiceConnection, Context.BIND_AUTO_CREATE)
withContext(mainDispatcher) { updateAutoTunnelTile() }
if (autoTunnelService.isCompleted) {
_autoTunnelActive.update { true }
return
}
runCatching {
autoTunnelService = CompletableDeferred()
startService(AutoTunnelService::class.java, !WireGuardAutoTunnel.isForeground())
_autoTunnelActive.update { true }
}
.onFailure {
Timber.e(it)
_autoTunnelActive.update { false }
}
withContext(mainDispatcher) { updateAutoTunnelTile() }
}
}
@@ -91,44 +84,43 @@ constructor(
autoTunnelMutex.withLock {
val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
if (_autoTunnelService.value == null) return
_autoTunnelService.value?.let { service ->
service.stop()
try {
context.unbindService(autoTunnelServiceConnection)
} finally {
_tunnelService.value = null
if (!autoTunnelService.isCompleted) return
runCatching {
val service = autoTunnelService.await()
service.stop()
_autoTunnelActive.update { false }
autoTunnelService = CompletableDeferred()
}
}
.onFailure { Timber.e(it) }
withContext(mainDispatcher) { updateAutoTunnelTile() }
}
}
suspend fun startTunnelForegroundService() {
if (_tunnelService.value != null) return
withContext(ioDispatcher) {
applicationScope.launch(ioDispatcher) {
val intent = Intent(context, TunnelForegroundService::class.java)
context.startForegroundService(intent)
context.bindService(intent, tunnelServiceConnection, Context.BIND_AUTO_CREATE)
fun startTunnelForegroundService() {
if (backgroundService.isCompleted) return
runCatching {
backgroundService = CompletableDeferred()
startService(
TunnelForegroundService::class.java,
!WireGuardAutoTunnel.isForeground(),
)
}
}
.onFailure { Timber.e(it) }
}
fun stopTunnelForegroundService() {
_tunnelService.value?.let { service ->
service.stop()
try {
context.unbindService(tunnelServiceConnection)
} finally {
_tunnelService.value = null
suspend fun stopTunnelForegroundService() {
if (!backgroundService.isCompleted) return
runCatching {
val service = backgroundService.await()
service.stop()
backgroundService = CompletableDeferred()
}
}
.onFailure { Timber.e(it) }
}
fun toggleAutoTunnel() {
applicationScope.launch(ioDispatcher) {
if (_autoTunnelService.value != null) stopAutoTunnel() else startAutoTunnel()
if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel()
}
}
@@ -139,12 +131,4 @@ constructor(
fun updateTunnelTile() {
context.requestTunnelTileServiceStateUpdate()
}
fun handleTunnelServiceDestroy() {
_tunnelService.update { null }
}
fun handleAutoTunnelServiceDestroy() {
_autoTunnelService.update { null }
}
}
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Notification
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
@@ -24,6 +23,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
@@ -64,12 +64,9 @@ class TunnelForegroundService : LifecycleService() {
private val jobsMutex = Mutex()
class LocalBinder(val service: TunnelForegroundService) : Binder()
private val binder = LocalBinder(this)
override fun onCreate() {
super.onCreate()
serviceManager.backgroundService.complete(this)
ServiceCompat.startForeground(
this@TunnelForegroundService,
NotificationManager.VPN_NOTIFICATION_ID,
@@ -78,13 +75,14 @@ class TunnelForegroundService : LifecycleService() {
)
}
override fun onBind(intent: Intent): IBinder {
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return binder
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
serviceManager.backgroundService.complete(this)
ServiceCompat.startForeground(
this@TunnelForegroundService,
NotificationManager.VPN_NOTIFICATION_ID,
@@ -165,12 +163,8 @@ class TunnelForegroundService : LifecycleService() {
} else {
pingJobs[tun]?.cancel() // Cancel any stale job
if (tun.isPingEnabled) {
if (tun.isStaticallyConfigured()) {
Timber.d("Skipping ping for statically configured tunnel")
} else {
pingJobs[tun] = startPingJob(tun)
Timber.d("Started ping job for ${tun.tunName}")
}
pingJobs[tun] = startPingJob(tun)
Timber.d("Started ping job for ${tun.tunName}")
}
}
}
@@ -279,7 +273,7 @@ class TunnelForegroundService : LifecycleService() {
}
override fun onDestroy() {
serviceManager.handleTunnelServiceDestroy()
serviceManager.backgroundService = CompletableDeferred()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
super.onDestroy()
}
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.ServiceCompat
@@ -29,6 +28,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
@@ -68,23 +68,21 @@ class AutoTunnelService : LifecycleService() {
private var killSwitchJob: Job? = null
class LocalBinder(val service: AutoTunnelService) : Binder()
private val binder = LocalBinder(this)
override fun onCreate() {
super.onCreate()
serviceManager.autoTunnelService.complete(this)
launchWatcherNotification()
}
override fun onBind(intent: Intent): IBinder {
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return binder
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this)
start()
return START_STICKY
}
@@ -107,7 +105,7 @@ class AutoTunnelService : LifecycleService() {
}
override fun onDestroy() {
serviceManager.handleAutoTunnelServiceDestroy()
serviceManager.autoTunnelService = CompletableDeferred()
restoreVpnKillSwitch()
super.onDestroy()
}
@@ -38,8 +38,8 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for auto tunnel tile")
lifecycleScope.launch {
serviceManager.autoTunnelService.collect {
if (it != null) return@collect setActive()
serviceManager.autoTunnelActive.collect {
if (it) return@collect setActive()
setInactive()
}
}
@@ -56,7 +56,7 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
if (serviceManager.autoTunnelService.value != null) {
if (serviceManager.autoTunnelActive.value) {
serviceManager.stopAutoTunnel()
setInactive()
} else {
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
@@ -10,11 +11,14 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import java.util.concurrent.ConcurrentHashMap
import kotlin.concurrent.thread
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
@@ -27,10 +31,6 @@ abstract class BaseTunnel(
private val serviceManager: ServiceManager,
) : TunnelProvider {
private val _errorEvents =
MutableSharedFlow<Pair<TunnelConf, BackendError>>(replay = 0, extraBufferCapacity = 1)
override val errorEvents = _errorEvents.asSharedFlow()
private val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap())
private val tunThreads = ConcurrentHashMap<Int, Thread>()
override val activeTunnels = activeTuns.asStateFlow()
@@ -45,34 +45,37 @@ abstract class BaseTunnel(
abstract fun stopBackend(tunnel: TunnelConf)
override suspend fun clearError(tunnelConf: TunnelConf) =
updateTunnelStatus(tunnelConf, TunnelStatus.Down)
override fun hasVpnPermission(): Boolean {
return serviceManager.hasVpnPermission()
}
protected suspend fun updateTunnelStatus(
tunnelConf: TunnelConf,
status: TunnelStatus? = null,
state: TunnelStatus? = null,
stats: TunnelStatistics? = null,
) {
tunStatusMutex.withLock {
activeTuns.update { currentTuns ->
val originalConf = currentTuns.getKeyById(tunnelConf.id) ?: tunnelConf
val existingState = currentTuns.getValueById(tunnelConf.id) ?: TunnelState()
val newState = status ?: existingState.status
activeTuns.update { current ->
val originalConf = current.getKeyById(tunnelConf.id) ?: tunnelConf
val existingState = current.getValueById(tunnelConf.id) ?: TunnelState()
val newState = state ?: existingState.status
if (newState == TunnelStatus.Down) {
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
cleanUpTunThread(tunnelConf)
currentTuns - originalConf
current - originalConf
} else if (existingState.status == newState && stats == null) {
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState")
currentTuns
current
} else {
val updated =
existingState.copy(
status = newState,
statistics = stats ?: existingState.statistics,
)
currentTuns + (originalConf to updated)
current + (originalConf to updated)
}
}
}
@@ -114,17 +117,23 @@ abstract class BaseTunnel(
if (this@BaseTunnel is UserspaceTunnel) stopActiveTunnels()
tunMutex.withLock {
tunThreads[tunnelConf.id] = thread {
runBlocking {
try {
Timber.d("Starting tunnel ${tunnelConf.id}...")
startTunnelInner(tunnelConf)
Timber.d("Started complete for tunnel ${tunnelConf.name}...")
} catch (e: InterruptedException) {
Timber.w(
"Tunnel start has been interrupted as ${tunnelConf.name} failed to start"
)
runCatching {
runBlocking {
try {
Timber.d("Starting tunnel ${tunnelConf.id}...")
startTunnelInner(tunnelConf)
Timber.d("Started complete for tunnel ${tunnelConf.name}...")
} catch (e: BackendError) {
Timber.e(e, "Failed to start tunnel ${tunnelConf.name} userspace")
updateTunnelStatus(tunnelConf, TunnelStatus.Error(e))
} catch (e: InterruptedException) {
Timber.w(
"Tunnel start has been interrupted as ${tunnelConf.name} failed to start"
)
}
}
}
}
.onFailure { Timber.w("Tunnel start has been interrupted") }
}
}
}
@@ -138,10 +147,11 @@ abstract class BaseTunnel(
Timber.d("Started for tun ${tunnelConf.id}...")
saveTunnelActiveState(tunnelConf, true)
serviceManager.startTunnelForegroundService()
} catch (e: BackendError) {
} catch (e: BackendException) {
Timber.e(e, "Failed to start backend for ${tunnelConf.name}")
_errorEvents.emit(tunnelConf to e)
updateTunnelStatus(tunnelConf, TunnelStatus.Down)
val backendError = e.toBackendError()
updateTunnelStatus(tunnelConf, TunnelStatus.Error(backendError))
throw backendError
}
}
@@ -153,27 +163,26 @@ abstract class BaseTunnel(
override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) {
if (tunnelConf == null) return stopActiveTunnels()
tunMutex.withLock {
if (activeTuns.isStarting(tunnelConf.id))
return handleStuckStartingTunnelShutdown(tunnelConf)
updateTunnelStatus(tunnelConf, TunnelStatus.Stopping(reason))
stopTunnelInner(tunnelConf)
try {
if (activeTuns.isStarting(tunnelConf.id))
return handleStuckStartingTunnelShutdown(tunnelConf)
updateTunnelStatus(tunnelConf, TunnelStatus.Stopping(reason))
stopTunnelInner(tunnelConf)
} catch (e: BackendError) {
Timber.e(e, "Failed to stop tunnel ${tunnelConf.id}")
updateTunnelStatus(tunnelConf, TunnelStatus.Error(e))
}
}
}
private suspend fun stopTunnelInner(tunnelConf: TunnelConf) {
try {
val tunnel = activeTuns.findTunnel(tunnelConf.id) ?: return
stopBackend(tunnel)
saveTunnelActiveState(tunnelConf, false)
removeActiveTunnel(tunnel)
} catch (e: BackendError) {
Timber.e(e, "Failed to stop tunnel ${tunnelConf.id}")
_errorEvents.emit(tunnelConf to e)
updateTunnelStatus(tunnelConf, TunnelStatus.Down)
}
val tunnel = activeTuns.findTunnel(tunnelConf.id) ?: return
stopBackend(tunnel)
saveTunnelActiveState(tunnelConf, false)
removeActiveTunnel(tunnel)
}
private fun handleServiceStateOnChange() {
private suspend fun handleServiceStateOnChange() {
if (activeTuns.value.isEmpty() && bouncingTunnelIds.isEmpty())
serviceManager.stopTunnelForegroundService()
}
@@ -184,15 +193,15 @@ abstract class BaseTunnel(
tunThreads[tunnel.id]?.let {
if (it.state != Thread.State.TERMINATED) {
it.interrupt()
updateTunnelStatus(tunnel, TunnelStatus.Down)
} else {
Timber.d("Thread already terminated")
}
}
} catch (e: Exception) {
Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}")
} finally {
updateTunnelStatus(tunnel, TunnelStatus.Down)
}
cleanUpTunThread(tunnel)
}
private fun cleanUpTunThread(tunnel: TunnelConf) {
@@ -212,7 +221,7 @@ abstract class BaseTunnel(
bouncingTunnelIds[tunnelConf.id] = reason
try {
stopTunnel(tunnelConf, reason)
delay(BOUNCE_DELAY)
delay(300L)
startTunnel(tunnelConf)
} finally {
bouncingTunnelIds.remove(tunnelConf.id)
@@ -226,8 +235,4 @@ abstract class BaseTunnel(
override suspend fun runningTunnelNames(): Set<String> =
activeTuns.value.keys.map { it.tunName }.toSet()
companion object {
const val BOUNCE_DELAY = 300L
}
}
@@ -5,7 +5,6 @@ import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.di.Userspace
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
@@ -16,7 +15,6 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
@@ -64,9 +62,6 @@ constructor(
initialValue = emptyMap(),
)
override val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>>
get() = tunnelProviderFlow.value.errorEvents
override val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason> =
tunnelProviderFlow.value.bouncingTunnelIds
@@ -74,6 +69,10 @@ constructor(
return userspaceTunnel.hasVpnPermission()
}
override suspend fun clearError(tunnelConf: TunnelConf) {
tunnelProviderFlow.value.clearError(tunnelConf)
}
override suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
tunnelProviderFlow.value.updateTunnelStatistics(tunnel)
}
@@ -1,13 +1,11 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
interface TunnelProvider {
@@ -48,11 +46,11 @@ interface TunnelProvider {
val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>>
val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>>
val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason>
fun hasVpnPermission(): Boolean
suspend fun clearError(tunnelConf: TunnelConf)
suspend fun updateTunnelStatistics(tunnel: TunnelConf)
}
@@ -50,9 +50,8 @@ constructor(
} catch (e: BackendException) {
Timber.e(e, "Failed to stop tunnel ${tunnel.id}")
throw e.toBackendError()
} finally {
handlePreviouslyEnabledVpnKillSwitch()
}
handlePreviouslyEnabledVpnKillSwitch()
}
// stop vpn kill switch if we need to resolve DNS for peer endpoints
@@ -70,7 +69,7 @@ constructor(
// restore vpn kill switch if needed
private fun handlePreviouslyEnabledVpnKillSwitch() {
// let auto tunnel handle this if it is active
if (serviceManager.autoTunnelService.value == null) {
if (!serviceManager.autoTunnelActive.value) {
previousBackendState?.let { (state, lanEnabled) ->
Timber.d("Restoring kill switch configuration")
val lan = if (lanEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
@@ -57,7 +57,7 @@ constructor(
withContext(ioDispatcher) {
Timber.i("Service worker started")
with(appDataRepository.settings.get()) {
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null)
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value)
return@with serviceManager.startAutoTunnel()
if (tunnelManager.activeTunnels.value.isEmpty())
tunnelManager.restorePreviousState()
@@ -4,6 +4,4 @@ import com.zaneschepke.wireguardautotunnel.data.model.GitHubRelease
interface GitHubApi {
suspend fun getLatestRelease(owner: String, repo: String): Result<GitHubRelease>
suspend fun getNightlyRelease(owner: String, repo: String): Result<GitHubRelease>
}
@@ -24,33 +24,4 @@ class KtorGitHubApi(private val client: HttpClient) : GitHubApi {
Result.failure(e)
}
}
override suspend fun getNightlyRelease(owner: String, repo: String): Result<GitHubRelease> {
return try {
// Fetch all releases
val releases: List<GitHubRelease> =
client.get("https://api.github.com/repos/$owner/$repo/releases").body()
// Find the first release with "nightly" in the tag_name (case-insensitive)
val nightlyRelease =
releases.firstOrNull { release ->
release.tagName.contains("nightly", ignoreCase = true)
}
if (nightlyRelease != null) {
Result.success(nightlyRelease)
} else {
Result.failure(Exception("No release with 'nightly' tag found"))
}
} catch (e: ClientRequestException) {
when (e.response.status) {
HttpStatusCode.Forbidden -> Result.failure(Exception("Rate limit exceeded"))
HttpStatusCode.NotFound ->
Result.failure(Exception("Repository or release not found"))
else -> Result.failure(e)
}
} catch (e: Exception) {
Result.failure(e)
}
}
}
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import android.content.Context
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppUpdate
@@ -17,7 +16,6 @@ import io.ktor.utils.io.readAvailable
import java.io.File
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
class GitHubUpdateRepository(
private val gitHubApi: GitHubApi,
@@ -29,24 +27,11 @@ class GitHubUpdateRepository(
) : UpdateRepository {
override suspend fun checkForUpdate(currentVersion: String): Result<AppUpdate?> =
withContext(ioDispatcher) {
Timber.i("Checking for update")
val release =
if (BuildConfig.VERSION_NAME.contains("nightly")) {
gitHubApi.getNightlyRelease(githubOwner, githubRepo)
} else {
gitHubApi.getLatestRelease(githubOwner, githubRepo)
}
release.map { release ->
val apkAsset =
release.assets.find { asset ->
asset.name.startsWith("wgtunnel-full-v") && asset.name.endsWith(".apk")
}
val newVersion =
apkAsset?.name?.removePrefix("wgtunnel-full-v")?.removeSuffix(".apk")
?: return@map null
Timber.i("Latest version: $newVersion, current version: $currentVersion")
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
gitHubApi.getLatestRelease(githubOwner, githubRepo).map { release ->
if (
NumberUtils.compareVersions(release.tagName.removePrefix("v"), currentVersion) >
0
) {
release.toAppUpdate()
} else {
null
@@ -60,10 +60,6 @@ data class TunnelConf(
return result
}
fun isStaticallyConfigured(): Boolean {
return toAmConfig().peers.all { it.endpoint.get().host.isValidIpv4orIpv6Address() }
}
fun copyWithCallback(
id: Int = this.id,
tunName: String = this.tunName,
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
sealed class TunnelStatus {
data class Error(val error: BackendError) : TunnelStatus()
data object Up : TunnelStatus()
@@ -12,7 +12,6 @@ class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics()
rxBytes = stats.rxBytes,
txBytes = stats.txBytes,
latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis,
resolvedEndpoint = stats.resolvedEndpoint,
)
}
}
@@ -8,7 +8,6 @@ abstract class TunnelStatistics {
val rxBytes: Long,
val txBytes: Long,
val latestHandshakeEpochMillis: Long,
val resolvedEndpoint: String,
)
abstract fun peerStats(peer: Key): PeerStats?
@@ -12,7 +12,6 @@ class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics
txBytes = peerStats.txBytes,
rxBytes = peerStats.rxBytes,
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis,
resolvedEndpoint = peerStats.resolvedEndpoint,
)
}
}
@@ -2,21 +2,22 @@ package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextOverflow
@@ -43,25 +44,32 @@ fun ExpandingRowListItem(
modifier =
Modifier.animateContentSize()
.clip(RoundedCornerShape(8.dp))
.background(
if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
else Color.Transparent
)
.then(
if (!isTv) {
Modifier.combinedClickable(
interactionSource = interactionSource,
indication = ripple(),
onClick = onClick,
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onHold()
},
onDoubleClick = onDoubleClick,
)
onClick = onClick,
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onHold()
},
onDoubleClick = onDoubleClick,
)
.indication(
interactionSource = interactionSource,
indication = ripple(),
)
} else Modifier
)
) {
LaunchedEffect(isSelected) {
if (isSelected) {
interactionSource.emit(PressInteraction.Press(Offset.Zero))
} else {
interactionSource.emit(
PressInteraction.Release(PressInteraction.Press(Offset.Zero))
)
}
}
Column {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
@@ -1,42 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.common.functions
import android.content.ClipData
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ClipboardHelper(
private val clipboard: Clipboard,
private val coroutineScope: CoroutineScope,
private val dispatcher: CoroutineDispatcher = Dispatchers.Main,
) {
fun copy(text: String, label: String = "") {
coroutineScope.launch(dispatcher) {
val clipData = ClipData.newPlainText(label, text)
clipboard.setClipEntry(ClipEntry(clipData))
}
}
fun paste(onResult: (String?) -> Unit) {
coroutineScope.launch(dispatcher) {
val entry = clipboard.getClipEntry()
val text = entry?.clipData?.getItemAt(0)?.text?.toString()
onResult(text)
}
}
}
@Composable
fun rememberClipboardHelper(
coroutineScope: CoroutineScope = rememberCoroutineScope()
): ClipboardHelper {
val clipboard = LocalClipboard.current
return remember(clipboard, coroutineScope) { ClipboardHelper(clipboard, coroutineScope) }
}
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.PublicOff
import androidx.compose.material.icons.outlined.AirplanemodeActive
import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.SignalCellular4Bar
import androidx.compose.material3.MaterialTheme
@@ -95,7 +95,7 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnEthernet) },
),
SelectionItem(
leadingIcon = Icons.Outlined.PublicOff,
leadingIcon = Icons.Outlined.AirplanemodeActive,
title = {
Text(
stringResource(R.string.stop_on_no_internet),
@@ -22,15 +22,16 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@@ -47,7 +48,7 @@ fun WifiTunnelingItems(
isWifiNameReadable: () -> Boolean,
): List<SelectionItem> {
val context = LocalContext.current
val clipboardHelper = rememberClipboardHelper()
val clipboard = LocalClipboardManager.current
val baseItems =
listOf(
@@ -70,41 +71,29 @@ fun WifiTunnelingItems(
)
},
description = {
val wifiInfo by
val wifiName by
remember(uiState.networkStatus) {
derivedStateOf {
(uiState.networkStatus as? NetworkStatus.Connected)
?.takeIf { it.wifiConnected }
.let { Pair(it?.wifiSsid, it?.securityType) }
?.wifiSsid
}
}
val (wifiName, securityType) = wifiInfo
Column {
Text(
text =
wifiName?.let { stringResource(R.string.wifi_name_template, it) }
?: stringResource(R.string.inactive),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier =
Modifier.clickable { wifiName?.let { clipboardHelper.copy(it) } },
)
securityType?.let {
Text(
text = stringResource(R.string.security_template, it.name),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
Text(
text =
wifiName?.let { stringResource(R.string.wifi_name_template, it) }
?: stringResource(R.string.inactive),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier =
Modifier.clickable {
wifiName?.let { clipboard.setText(AnnotatedString(it)) }
},
)
},
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnWifi) },
),
@@ -7,12 +7,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ExportTunnelsBottomSheet
@@ -29,7 +29,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable
fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: AppViewModel) {
val navController = LocalNavController.current
val clipboard = rememberClipboardHelper()
val clipboard = LocalClipboardManager.current
var showUrlImportDialog by remember { mutableStateOf(false) }
@@ -90,9 +90,8 @@ fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: Ap
requestPermissionLauncher.launch(android.Manifest.permission.CAMERA)
},
onClipboardClick = {
clipboard.paste { result ->
if (result != null)
viewModel.handleEvent(AppEvent.ImportTunnelFromClipboard(result))
clipboard.getText()?.text?.let {
viewModel.handleEvent(AppEvent.ImportTunnelFromClipboard(it))
}
},
onManualImportClick = {
@@ -7,7 +7,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -19,7 +18,6 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.getValueById
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@@ -37,19 +35,12 @@ fun TunnelList(
onToggleTunnel: (TunnelConf, Boolean) -> Unit,
viewModel: AppViewModel,
) {
val isTv = LocalIsAndroidTV.current
val context = LocalContext.current
val navController = LocalNavController.current
val collator = Collator.getInstance(Locale.getDefault())
val sortedTunnels =
remember(appUiState.tunnels) {
appUiState.tunnels.sortedWith(
compareBy(
// primary tunnel first
{ !it.isPrimaryTunnel },
{ collator.compare(it.tunName, "") },
)
)
appUiState.tunnels.sortedWith(compareBy(collator) { it.tunName })
}
LazyColumn(
@@ -58,7 +49,7 @@ fun TunnelList(
modifier =
modifier
.pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput }
.overscroll(rememberOverscrollEffect()),
.overscroll(ScrollableDefaults.overscrollEffect()),
state = rememberLazyListState(0, appUiState.tunnels.count()),
userScrollEnabled = true,
reverseLayout = false,
@@ -80,12 +71,8 @@ fun TunnelList(
tunnel = tunnel,
tunnelState = tunnelState,
onClick = {
if (selectedTunnels.isNotEmpty() && !isTv) {
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
} else {
navController.navigate(Route.TunnelOptions(tunnel.id))
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
}
navController.navigate(Route.TunnelOptions(tunnel.id))
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
},
onDoubleClick = {
viewModel.handleEvent(AppEvent.ToggleTunnelStatsExpanded(tunnel.id))
@@ -94,7 +81,6 @@ fun TunnelList(
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(it))
},
onSwitchClick = { checked -> onToggleTunnel(tunnel, checked) },
isTv = isTv,
)
}
}
@@ -26,6 +26,7 @@ import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
@Composable
@@ -39,8 +40,9 @@ fun TunnelRowItem(
onDoubleClick: () -> Unit,
onToggleSelectedTunnel: (TunnelConf) -> Unit,
onSwitchClick: (Boolean) -> Unit,
isTv: Boolean,
) {
val isTv = LocalIsAndroidTV.current
val leadingIconColor =
remember(state) {
if (state.status.isUp()) tunnelState.statistics.asColor() else Color.Gray
@@ -8,9 +8,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -24,90 +21,49 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.toThreeDecimalPlaceSt
@Composable
fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConf: TunnelConf) {
val config = TunnelConf.configFromAmQuick(tunnelConf.wgQuick)
Column(
modifier = Modifier.fillMaxWidth().padding(start = 45.dp, bottom = 10.dp, end = 10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.Start,
) {
config.peers.forEach { peer ->
val peerId = remember { peer.publicKey.toBase64().subSequence(0, 3).toString() + "***" }
val endpoint =
remember(statistics) { statistics?.peerStats(peer.publicKey)?.resolvedEndpoint }
val peerRxMB by
remember(statistics) {
derivedStateOf {
statistics
?.peerStats(peer.publicKey)
?.rxBytes
?.let { NumberUtils.bytesToMB(it) }
?.toThreeDecimalPlaceString()
config.peers.forEach { peer ->
Row(
modifier = Modifier.fillMaxWidth().padding(end = 10.dp, bottom = 10.dp, start = 45.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start),
) {
val peerId = peer.publicKey.toBase64().subSequence(0, 3).toString() + "***"
val peerRx = statistics?.peerStats(peer.publicKey)?.rxBytes ?: 0
val peerTx = statistics?.peerStats(peer.publicKey)?.txBytes ?: 0
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString()
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString()
val handshake =
statistics?.peerStats(peer.publicKey)?.latestHandshakeEpochMillis?.let {
if (it == 0L) {
stringResource(R.string.never)
} else {
"${NumberUtils.getSecondsBetweenTimestampAndNow(it)} ${stringResource(R.string.sec)}"
}
}
val peerTxMB by
remember(statistics) {
derivedStateOf {
statistics
?.peerStats(peer.publicKey)
?.txBytes
?.let { NumberUtils.bytesToMB(it) }
?.toThreeDecimalPlaceString()
}
}
val handshake by
remember(statistics) {
derivedStateOf {
statistics?.peerStats(peer.publicKey)?.latestHandshakeEpochMillis?.let {
if (it == 0L) {
null
} else {
"${NumberUtils.getSecondsBetweenTimestampAndNow(it)}"
}
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
) {
} ?: stringResource(R.string.never)
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(
stringResource(R.string.peer).lowercase() + ": $peerId",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
Text(
stringResource(R.string.handshake) +
": ${if(handshake == null) stringResource(R.string.never) else handshake + " " + stringResource(R.string.sec)}",
"tx: $peerTxMB MB",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(
"rx: ${peerRxMB ?: 0.00} MB",
stringResource(R.string.handshake) + ": $handshake",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
Text(
"tx: ${peerTxMB ?: 0.00} MB",
"rx: $peerRxMB MB",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
}
if (endpoint != null) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
) {
Text(
"endpoint: $endpoint",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
}
}
}
}
}
@@ -16,15 +16,16 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
@Composable
@@ -37,7 +38,7 @@ fun InterfaceFields(
onInterfaceChange: (InterfaceProxy) -> Unit,
) {
val keyboardController = LocalSoftwareKeyboardController.current
val clipboardManager = rememberClipboardHelper()
val clipboardManager = LocalClipboardManager.current
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
@@ -52,9 +53,8 @@ fun InterfaceFields(
if (isAuthenticated) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
IconButton(
enabled = true,
enabled = isAuthenticated,
onClick = {
if (!isAuthenticated) return@IconButton showAuthPrompt()
val keypair = com.wireguard.crypto.KeyPair()
onInterfaceChange(
interfaceState.copy(
@@ -88,7 +88,9 @@ fun InterfaceFields(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
trailingIcon = {
IconButton(onClick = { clipboardManager.copy(interfaceState.publicKey) }) {
IconButton(
onClick = { clipboardManager.setText(AnnotatedString(interfaceState.publicKey)) }
) {
Icon(Icons.Rounded.ContentCopy, stringResource(R.string.copy_public_key))
}
},
@@ -1,15 +1,12 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel
import androidx.compose.foundation.layout.Box
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
@@ -37,16 +34,21 @@ fun SplitTunnelScreen(
appViewModel.handleEvent(AppEvent.PopBackStack(true))
}
}
if (uiState.loading) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(modifier = Modifier.size(30.dp), strokeWidth = 5.dp)
Crossfade(
targetState = uiState.loading,
animationSpec = tween(200),
modifier = Modifier.fillMaxSize(),
) { isLoading ->
if (isLoading) {
SplitTunnelSkeleton()
} else {
SplitTunnelContent(
uiState = uiState,
onSplitOptionChange = viewModel::updateSplitOption,
onAppSelectionToggle = viewModel::toggleAppSelection,
onQueryChange = viewModel::onSearchQuery,
)
}
} else {
SplitTunnelContent(
uiState = uiState,
onSplitOptionChange = viewModel::updateSplitOption,
onAppSelectionToggle = viewModel::toggleAppSelection,
onQueryChange = viewModel::onSearchQuery,
)
}
}
@@ -0,0 +1,92 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
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.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.animation.ShimmerEffect
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@Composable
fun SplitTunnelSkeleton() {
val shimmerBrush = ShimmerEffect()
Column(
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth().padding(top = 24.dp),
) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp).height(45.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
repeat(3) {
Box(
modifier =
Modifier.weight(1f)
.height(45.dp)
.clip(RoundedCornerShape(8.dp))
.background(shimmerBrush)
)
}
}
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp).height(45.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Box(
modifier =
Modifier.height(45.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(shimmerBrush)
)
}
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
contentPadding = PaddingValues(top = 10.dp),
modifier = Modifier.fillMaxWidth(),
) {
items(20) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier =
Modifier.size(iconSize).clip(CircleShape).background(shimmerBrush)
)
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier =
Modifier.height(20.dp)
.weight(1f)
.clip(RoundedCornerShape(4.dp))
.background(shimmerBrush)
)
Spacer(modifier = Modifier.width(16.dp))
Box(modifier = Modifier.size(24.dp).clip(CircleShape).background(shimmerBrush))
}
}
}
}
}
@@ -18,6 +18,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import java.text.Collator
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -49,6 +50,7 @@ constructor(
tunnelId?.let { loadInitialState(it) }
}
// TODO improve this loading experience
private fun loadInitialState(tunnelId: Int) =
viewModelScope.launch {
val tunnel = tunnelRepository.getById(tunnelId) ?: return@launch
@@ -64,7 +66,7 @@ constructor(
val installedPackages = packages.map { it.packageName }.toSet()
// Remove uninstalled apps
// remove uninstalled apps
proxyInterface.includedApplications.retainAll { it in installedPackages }
proxyInterface.excludedApplications.retainAll { it in installedPackages }
@@ -96,13 +98,12 @@ constructor(
selected,
)
}
.sortedWith(
compareByDescending<Pair<TunnelApp, Boolean>> { it.second }
.thenBy(collator) { it.first.name }
)
.sortedWith(compareBy(collator) { it.first.name })
allTunneledApps = tunneledApps
delay(500)
_uiState.update {
SplitTunnelUiState(
loading = false,
@@ -8,19 +8,20 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable
fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
val clipboardManager = rememberClipboardHelper()
val clipboardManager = LocalClipboardManager.current
return SelectionItem(
leadingIcon = Icons.Filled.SmartToy,
@@ -41,7 +42,8 @@ fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionIt
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.clickable { clipboardManager.copy(key) },
modifier =
Modifier.clickable { clipboardManager.setText(AnnotatedString(key)) },
)
}
}
@@ -23,21 +23,25 @@ fun DisplayScreen(appUiState: AppUiState, viewModel: AppViewModel) {
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().padding(top = 24.dp).padding(horizontal = 24.dp),
) {
enumValues<Theme>().forEach {
val title =
when (it) {
Theme.DARK -> stringResource(R.string.dark)
Theme.LIGHT -> stringResource(R.string.light)
Theme.AUTOMATIC -> stringResource(R.string.automatic)
Theme.DYNAMIC -> stringResource(R.string.dynamic)
Theme.DARKER -> stringResource(R.string.darker)
Theme.AMOLED -> stringResource(R.string.amoled)
}
IconSurfaceButton(
title = title,
onClick = { viewModel.handleEvent(AppEvent.SetTheme(it)) },
selected = appUiState.appState.theme == it,
)
}
IconSurfaceButton(
title = stringResource(R.string.automatic),
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.AUTOMATIC)) },
selected = appUiState.appState.theme == Theme.AUTOMATIC,
)
IconSurfaceButton(
title = stringResource(R.string.light),
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.LIGHT)) },
selected = appUiState.appState.theme == Theme.LIGHT,
)
IconSurfaceButton(
title = stringResource(R.string.dark),
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.DARK)) },
selected = appUiState.appState.theme == Theme.DARK,
)
IconSurfaceButton(
title = stringResource(R.string.dynamic),
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.DYNAMIC)) },
selected = appUiState.appState.theme == Theme.DYNAMIC,
)
}
}
@@ -11,16 +11,17 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
@Composable
fun LogItem(log: LogMessage) {
val clipboardManager = rememberClipboardHelper()
val clipboardManager = LocalClipboardManager.current
val fontSize = 10.sp
Row(
@@ -31,7 +32,7 @@ fun LogItem(log: LogMessage) {
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { clipboardManager.copy(log.toString()) },
onClick = { clipboardManager.setText(AnnotatedString(log.toString())) },
),
) {
Text(text = log.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize)
@@ -15,7 +15,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.google.zxing.client.android.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
@@ -23,8 +23,8 @@ import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.ContactSupportOptions
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.GeneralSupportOptions
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateSection
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.canInstallPackages
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.requestInstallPackagesPermission
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@@ -54,10 +54,6 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
InfoDialog(
onDismiss = { viewModel.handleUpdateShown() },
onAttest = {
if (BuildConfig.FLAVOR != "full") {
uiState.appUpdate?.apkUrl?.let { context.openWebUrl(it) }
return@InfoDialog
}
if (context.canInstallPackages()) {
viewModel.handleDownloadAndInstallApk()
} else {
@@ -84,12 +80,7 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
}
}
},
confirmText = {
Text(
if (BuildConfig.FLAVOR != "full") stringResource(R.string.download)
else stringResource(R.string.download_and_install)
)
},
confirmText = { Text(stringResource(R.string.download_and_install)) },
)
}
@@ -117,21 +108,17 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
) {
GroupLabel(
stringResource(R.string.thank_you),
modifier = Modifier.padding(horizontal = 12.dp).padding(bottom = 12.dp),
modifier = Modifier.padding(horizontal = 12.dp),
)
UpdateSection(
onUpdateCheck = {
if (
BuildConfig.DEBUG ||
BuildConfig.VERSION_NAME.contains("beta") ||
BuildConfig.FLAVOR == "google"
)
return@UpdateSection context.showToast(R.string.update_check_unsupported)
context.showToast(R.string.checking_for_update)
viewModel.handleUpdateCheck()
}
)
SectionDivider()
if (BuildConfig.BUILD_TYPE == Constants.RELEASE) {
UpdateSection(
onUpdateCheck = {
context.showToast(R.string.checking_for_update)
viewModel.handleUpdateCheck()
}
)
SectionDivider()
}
GeneralSupportOptions(context)
SectionDivider()
ContactSupportOptions(context)
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CloudDownload
import androidx.compose.material.icons.rounded.CloudDownload
@@ -12,6 +11,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.util.Constants
@Composable
fun UpdateSection(onUpdateCheck: () -> Unit = {}) {
@@ -26,20 +26,16 @@ fun UpdateSection(onUpdateCheck: () -> Unit = {}) {
)
},
description = {
Column {
SelectionItemLabel(
stringResource(
R.string.version_template,
"v${BuildConfig.VERSION_NAME +
if(BuildConfig.DEBUG) "-debug" else "" }",
),
SelectionLabelType.DESCRIPTION,
)
SelectionItemLabel(
stringResource(R.string.flavor_template, BuildConfig.FLAVOR),
SelectionLabelType.DESCRIPTION,
)
}
val versionName =
if (BuildConfig.BUILD_TYPE == Constants.RELEASE) {
"v${BuildConfig.VERSION_NAME}"
} else {
"v${BuildConfig.VERSION_NAME}-${BuildConfig.BUILD_TYPE}"
}
SelectionItemLabel(
stringResource(R.string.version_template, versionName),
SelectionLabelType.DESCRIPTION,
)
},
onClick = onUpdateCheck,
)
@@ -10,9 +10,6 @@ val Plantation = Color(0xFF264A49)
val Shark = Color(0xFF21272A)
val BalticSea = Color(0xFF1C1B1F)
// amoled
val ElectricTeal = Color(0xFF4DD0E1)
// Status colors
val SilverTree = Color(0xFF6DB58B)
val Brick = Color(0xFFCE4257)
@@ -44,8 +44,6 @@ enum class Theme {
AUTOMATIC,
LIGHT,
DARK,
DARKER,
AMOLED,
DYNAMIC,
}
@@ -61,18 +59,6 @@ fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composabl
isDark = true
DarkColorScheme
}
Theme.DARKER -> {
isDark = true
DarkColorScheme.copy(surface = BalticSea, background = BalticSea)
}
Theme.AMOLED -> {
isDark = true
DarkColorScheme.copy(
surface = Color.Black,
background = Color.Black,
primary = ElectricTeal,
)
}
Theme.LIGHT -> {
isDark = false
LightColorScheme
@@ -5,7 +5,6 @@ import java.math.BigDecimal
import java.time.Duration
import java.time.Instant
import kotlin.math.pow
import timber.log.Timber
object NumberUtils {
private const val BYTES_IN_KB = 1024.0
@@ -42,13 +41,8 @@ object NumberUtils {
}
fun compareVersions(newVersion: String, currentVersion: String): Int {
try {
val newSemver = Semver(newVersion, Semver.SemverType.LOOSE)
val currentSemver = Semver(currentVersion, Semver.SemverType.LOOSE)
return newSemver.compareTo(currentSemver)
} catch (e: Exception) {
Timber.e(e, "Failed to compare versions $newVersion and $currentVersion")
return 0
}
val newSemver = Semver(newVersion, Semver.SemverType.LOOSE)
val currentSemver = Semver(currentVersion, Semver.SemverType.LOOSE)
return newSemver.compareTo(currentSemver)
}
}
@@ -1,29 +1,14 @@
package com.zaneschepke.wireguardautotunnel.util.extensions
import java.util.regex.Pattern
import timber.log.Timber
val hasNumberInParentheses = """^(.+?)\((\d+)\)$""".toRegex()
fun String.isValidIpv4orIpv6Address(): Boolean {
val sanitized = removeSurrounding("[", "]")
val ipv6Pattern =
Regex(
"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:)" +
"{1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]" +
"{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:" +
"[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4})" +
"{1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}" +
":((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]" +
"{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}" +
"[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:)" +
"{1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"
)
val ipv4Pattern =
Regex(
"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" +
"(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
)
return ipv4Pattern.matches(sanitized) || ipv6Pattern.matches(sanitized)
val ipv4Pattern = Pattern.compile("^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\$")
val ipv6Pattern = Pattern.compile("^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}\$")
return ipv4Pattern.matcher(this).matches() || ipv6Pattern.matcher(this).matches()
}
fun String.hasNumberInParentheses(): Boolean {
@@ -39,8 +39,6 @@ import java.time.Instant
import java.util.*
import javax.inject.Inject
import javax.inject.Provider
import kotlin.collections.component1
import kotlin.collections.component2
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
@@ -58,7 +56,7 @@ constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
@AppShell private val rootShell: Provider<RootShell>,
val tunnelManager: TunnelManager,
private val tunnelManager: TunnelManager,
private val serviceManager: ServiceManager,
private val logReader: LogReader,
private val fileUtils: FileUtils,
@@ -88,7 +86,7 @@ constructor(
appDataRepository.tunnels.flow,
appDataRepository.appState.flow,
tunnelManager.activeTunnels,
serviceManager.autoTunnelService.map { it != null },
serviceManager.autoTunnelActive,
networkMonitor.networkStatusFlow,
) { array ->
val settings = array[0] as AppSettings
@@ -208,6 +206,7 @@ constructor(
is AppEvent.ShowMessage -> handleShowMessage(event.message)
is AppEvent.PopBackStack ->
_appViewState.update { it.copy(popBackStack = event.pop) }
is AppEvent.ClearTunnelError -> tunnelManager.clearError(event.tunnel)
AppEvent.ToggleRemoteControl -> handleToggleRemoteControl(state.appState)
AppEvent.ClearSelectedTunnels -> clearSelectedTunnels()
is AppEvent.SetShowModal ->
@@ -266,9 +265,6 @@ constructor(
}
}
private fun handleTunnelErrors() =
viewModelScope.launch { tunnelManager.errorEvents.collect { errorEvent -> } }
private suspend fun handleAppReadyCheck(tunnels: List<TunnelConf>) {
if (tunnels.size == appDataRepository.tunnels.count()) {
_appViewState.update { it.copy(isAppReady = true) }
@@ -106,6 +106,8 @@ sealed class AppEvent {
data class ShowMessage(val message: StringValue) : AppEvent()
data class ClearTunnelError(val tunnel: TunnelConf) : AppEvent()
data class PopBackStack(val pop: Boolean) : AppEvent()
data class SetBottomSheet(val showSheet: AppViewState.BottomSheet) : AppEvent()
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

@@ -0,0 +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"/>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

+1 -6
View File
@@ -220,9 +220,7 @@
<string name="tunnel_error_template">Tunnel failed with: %1$s</string>
<string name="wifi_name_template">Active: %1$s</string>
<string name="remote_key_template">Key: %1$s</string>
<string name="version_template">Version: %1$s</string>
<string name="security_template">Security: %1$s</string>
<string name="flavor_template">Flavor: %1$s</string>
<string name="version_template">Current version: %1$s</string>
<string name="config_error">config error</string>
<string name="dns_resolve_error">dns resolution error</string>
<string name="invalid_config_error">invalid_config_error</string>
@@ -255,7 +253,4 @@
<string name="install_updated_permission">This app needs permission to install updates.</string>
<string name="allow">Allow</string>
<string name="licenses">Licenses</string>
<string name="update_check_unsupported">Update check not supported this build type.</string>
<string name="darker">Darker</string>
<string name="amoled">AMOLED</string>
</resources>
-5
View File
@@ -6,8 +6,3 @@ repositories {
google()
mavenCentral()
}
dependencies {
implementation("org.semver4j:semver4j:5.6.0")
implementation("org.ajoberstar.grgit:grgit-core:5.3.0")
}
+8 -3
View File
@@ -1,16 +1,21 @@
object Constants {
const val VERSION_NAME = "3.9.0"
const val VERSION_NAME = "3.8.3"
const val JVM_TARGET = "17"
const val VERSION_CODE = 38900
const val VERSION_CODE = 38300
const val TARGET_SDK = 35
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
const val APP_NAME = "wgtunnel"
// build types
const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD"
const val KEY_ALIAS_VAR = "SIGNING_KEY_ALIAS"
const val KEY_PASS_VAR = "SIGNING_KEY_PASSWORD"
const val KEY_STORE_PATH_VAR = "KEY_STORE_PATH"
const val RELEASE = "release"
const val NIGHTLY = "nightly"
const val PRERELEASE = "prerelease"
const val TYPE = "type"
val allowedLicenses = listOf("MIT", "Apache-2.0", "BSD-3-Clause")
const val XZING_LICENSE_URL: String = "https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING"
+72 -97
View File
@@ -1,7 +1,76 @@
import org.ajoberstar.grgit.Grgit
import org.gradle.api.Project
import org.semver4j.Semver
import java.io.File
import java.util.*
fun Project.getCurrentFlavor(): String {
val taskRequestsStr = gradle.startParameter.taskRequests.toString()
val pattern: java.util.regex.Pattern =
if (taskRequestsStr.contains(":app:assemble")) {
java.util.regex.Pattern.compile(":app:assemble(\\w+)(Release|Debug)")
} else {
java.util.regex.Pattern.compile(":app:bundle(\\w+)(Release|Debug)")
}
val matcher = pattern.matcher(taskRequestsStr)
val flavor =
if (matcher.find()) {
matcher.group(1).lowercase()
} else {
print("NO FLAVOR FOUND")
""
}
return flavor
}
fun Project.getBuildTaskName(): String {
val taskRequestsStr = gradle.startParameter.taskRequests[0].toString()
return taskRequestsStr.also {
project.logger.lifecycle("Build task: $it")
}
}
fun getLocalProperty(key: String, file: String = "local.properties"): String? {
val properties = 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 Project.getSigningProperties(): Properties {
return Properties().apply {
// created local file for signing details
try {
load(file("signing.properties").reader())
} catch (_: Exception) {
load(file("signing_template.properties").reader())
}
}
}
fun Project.getStoreFile(): File {
return file(
System.getenv()
.getOrDefault(
Constants.KEY_STORE_PATH_VAR,
getSigningProperties().getProperty(Constants.KEY_STORE_PATH_VAR),
),
)
}
fun Project.getSigningProperty(property: String): String {
// try to get secrets from env first for pipeline build, then properties file for local
return System.getenv()
.getOrDefault(
property,
getSigningProperties().getProperty(property),
)
}
fun Project.languageList(): List<String> {
return fileTree("../app/src/main/res") { include("**/strings.xml") }
@@ -15,100 +84,6 @@ fun Project.languageList(): List<String> {
.toList() + "en"
}
// Get the Git commit hash
fun Project.getGitCommitHash(): String {
var grgit: Grgit? = null
try {
grgit = Grgit.open(mapOf("currentDir" to projectDir))
return grgit.head().abbreviatedId
} catch (e: Exception) {
logger.warn("Failed to get Git commit hash: ${e.message}. Using fallback.")
return "unknown"
} finally {
grgit?.close()
}
}
// Get commit count since last commit for versionCode increment
fun Project.getCommitCountSinceLastCommit(): Int {
var grgit: Grgit? = null
try {
grgit = Grgit.open(mapOf("currentDir" to projectDir))
val headCommit = grgit.head()
val log = grgit.log(mapOf(
"includes" to listOf(headCommit.id)
))
return log.size
} catch (e: Exception) {
logger.warn("Failed to get commit count: ${e.message}. Using fallback.")
return 0
} finally {
grgit?.close()
}
}
// Get versionCode increment for nightly/pre-release
fun Project.getVersionCodeIncrement(): Int {
val isNightlyBuild = gradle.startParameter.taskNames.any { it.lowercase().contains("nightly") }
val isPreReleaseBuild = gradle.startParameter.taskNames.any { it.lowercase().contains("prerelease") }
if (!isNightlyBuild && !isPreReleaseBuild) return 0
return System.getenv("GITHUB_RUN_NUMBER")?.toIntOrNull()
?: System.getenv("CI_BUILD_NUMBER")?.toIntOrNull()
?: getCommitCountSinceLastCommit()
}
// Compute versionName dynamic bumping for nightly/pre-release
fun Project.computeVersionName(): String {
val isNightlyBuild = isNightlyBuild()
val isPreReleaseBuild = isPrereleaseBuild()
// Static version from Constants.kt
val baseVersion = Semver.parse(Constants.VERSION_NAME) ?: Semver.of(0, 0, 0)
return when {
isNightlyBuild -> {
// Bump patch for nightly
val nightlyVersion = Semver.of(
baseVersion.major,
baseVersion.minor,
baseVersion.patch + 1
)
"${nightlyVersion}-nightly+git.${getGitCommitHash()}"
}
isPreReleaseBuild -> {
// Bump minor for pre-release
val preReleaseVersion = Semver.of(
baseVersion.major,
baseVersion.minor,
0 + 1,
)
"${preReleaseVersion}-beta+git.${getGitCommitHash()}"
}
else -> Constants.VERSION_NAME
}
}
fun Project.isNightlyBuild(): Boolean {
return gradle.startParameter.taskNames.any { it.lowercase().contains(Constants.NIGHTLY) }
}
fun Project.isPrereleaseBuild(): Boolean {
return gradle.startParameter.taskNames.any { it.lowercase().contains(Constants.PRERELEASE) }
}
// Compute versionCode (static baseline, dynamic bumping for nightly/pre-release)
fun Project.computeVersionCode(): Int {
val isNightlyBuild = isNightlyBuild()
val isPreReleaseBuild = isPrereleaseBuild()
var versionCode = Constants.VERSION_CODE
if (isPreReleaseBuild) {
versionCode += 100 // Minor bump
}
if (isNightlyBuild) {
versionCode += 1 // Patch bump
}
return versionCode + getVersionCodeIncrement()
}
@@ -1,19 +0,0 @@
import java.io.File
import java.io.FileInputStream
import java.util.Properties
object LocalProperties {
private val properties by lazy {
val props = Properties()
val file = File("local.properties")
if (file.exists()) {
FileInputStream(file).use { props.load(it) }
}
props
}
fun get(key: String): String? = properties.getProperty(key)
fun getOrDefault(key: String, default: String): String = properties.getProperty(key, default)
}
+4 -4
View File
@@ -4,25 +4,25 @@ platform :android do
desc 'Deploy a new internal version to the Google Play Store'
lane :internal do
gradle(task: "clean bundleGoogleRelease")
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 bundleGoogleRelease")
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 bundleGoogleRelease")
gradle(task: "clean bundleGeneralRelease")
upload_to_play_store(track: 'beta', skip_upload_apk: true)
end
desc "Deploy a new version to the Google Play"
lane :production do
gradle(task: "clean bundleGoogleRelease")
gradle(task: "clean bundleGeneralRelease")
upload_to_play_store(skip_upload_apk: true)
end
@@ -1,6 +0,0 @@
What's new:
- Fix Android TV Banners
- Add multi-select for tunnels
- Add in-app update checker
- Add license screen
- Various bug fixes
+1 -1
View File
@@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+10 -10
View File
@@ -1,15 +1,16 @@
[versions]
accompanist = "0.37.2"
activityCompose = "1.10.1"
amneziawgAndroid = "1.3.10"
amneziawgAndroid = "1.3.8"
androidx-junit = "1.2.1"
appcompat = "1.7.0"
biometricKtx = "1.2.0-alpha05"
markdownCompose = "0.5.7"
coreKtx = "1.16.0"
datastorePreferences = "1.1.4"
desugar_jdk_libs = "2.1.5"
espressoCore = "3.6.1"
hiltAndroid = "2.56.2"
hiltAndroid = "2.56.1"
hiltCompiler = "1.2.0"
junit = "4.13.2"
kotlinx-serialization-json = "1.8.1"
@@ -19,17 +20,16 @@ material3 = "1.3.2"
navigationCompose = "2.8.9"
pinLockCompose = "1.0.4"
qrcodeKotlin = "4.4.1"
roomVersion = "2.7.1"
roomVersion = "2.7.0"
semver4j = "3.1.0"
slf4jAndroid = "1.7.36"
timber = "5.0.1"
tunnel = "1.2.16"
androidGradlePlugin = "8.9.2"
tunnel = "1.2.14"
androidGradlePlugin = "8.9.1"
kotlin = "2.1.20"
ksp = "2.1.20-2.0.0"
composeBom = "2025.04.01"
composeBom = "2025.04.00"
compose = "1.7.8"
workRuntimeKtxVersion = "2.10.1"
workRuntimeKtxVersion = "2.10.0"
zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1"
gradlePlugins-grgit = "5.3.0"
@@ -38,7 +38,7 @@ gradlePlugins-grgit = "5.3.0"
material = "1.12.0"
storage = "1.5.0"
ktfmt = "0.22.0"
licensee = "1.13.0"
licensee = "1.12.0"
[libraries]
@@ -95,12 +95,12 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorCli
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktorClientCore" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorClientCore" }
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle-runtime-compose" }
markdown-compose = { module = "com.github.jeziellago:compose-markdown", version.ref = "markdownCompose" }
material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" }
pin-lock-compose = { module = "com.zaneschepke:pin_lock_compose", version.ref = "pinLockCompose" }
qrcode-kotlin = { module = "io.github.g0dkar:qrcode-kotlin", version.ref = "qrcodeKotlin" }
semver4j = { module = "com.vdurmont:semver4j", version.ref = "semver4j" }
slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4jAndroid" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel" }
+2 -2
View File
@@ -5,10 +5,10 @@ plugins {
android {
namespace = "com.zaneschepke.networkmonitor"
compileSdk = Constants.TARGET_SDK
compileSdk = 34
defaultConfig {
minSdk = Constants.MIN_SDK
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
@@ -12,7 +12,6 @@ import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.os.Build
import com.wireguard.android.util.RootShell
import java.util.Collections
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
@@ -46,18 +45,10 @@ class AndroidNetworkMonitor(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
@get:Synchronized @set:Synchronized var currentSsid: String? = null
@get:Synchronized @set:Synchronized var securityType: WifiSecurityType? = null
@get:Synchronized @set:Synchronized var wifiConnected = false
// Track active Wi-Fi networks and last active network ID
private val activeNetworks = Collections.synchronizedSet(mutableSetOf<Network>())
data class WifiState(
val connected: Boolean = false,
val ssid: String? = null,
val securityType: WifiSecurityType? = null,
)
data class WifiState(val connected: Boolean = false, val ssid: String? = null)
data class TransportState(val connected: Boolean = false)
@@ -81,15 +72,15 @@ class AndroidNetworkMonitor(
suspend fun handleUnknownWifi() {
val newSsid = getWifiSsid()
val securityType = wifiManager?.getCurrentSecurityType()
// Only update if new SSID is valid; preserve existing valid SSID otherwise
if (newSsid != null && newSsid != WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(wifiConnected, currentSsid, securityType))
trySend(WifiState(connected = wifiConnected, ssid = currentSsid))
} else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(wifiConnected, currentSsid, securityType))
trySend(WifiState(connected = wifiConnected, ssid = currentSsid))
}
Timber.d("handleUnknownWifi: currentSsid=$currentSsid")
}
val locationPermissionReceiver =
@@ -148,34 +139,18 @@ class AndroidNetworkMonitor(
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Timber.d("Wi-Fi onAvailable: network=$network")
activeNetworks.add(network)
launch {
currentSsid = getWifiSsid()
securityType = wifiManager?.getCurrentSecurityType()
wifiConnected = true
trySend(
WifiState(
connected = true,
ssid = currentSsid,
securityType = securityType,
)
)
trySend(WifiState(connected = true, ssid = currentSsid))
}
}
override fun onLost(network: Network) {
Timber.d("Wi-Fi onLost: network=$network")
activeNetworks.remove(network)
if (activeNetworks.isEmpty()) {
Timber.d(
"All Wi-Fi networks disconnected, clearing currentSsid and wifiConnected"
)
currentSsid = null
wifiConnected = false
trySend(WifiState(connected = false, ssid = null, securityType = null))
} else {
Timber.d("Wi-Fi onLost, but still connected to other networks, ignoring")
}
currentSsid = null
wifiConnected = false
trySend(WifiState(connected = false, ssid = null))
}
}
@@ -253,7 +228,6 @@ class AndroidNetworkMonitor(
if (hasAnyConnection) {
NetworkStatus.Connected(
wifiSsid = wifi.ssid,
securityType = wifi.securityType,
wifiConnected = wifi.connected,
cellularConnected = cellular.connected,
ethernetConnected = ethernet.connected,
@@ -1,7 +1,5 @@
package com.zaneschepke.networkmonitor
import android.net.wifi.WifiManager
import android.os.Build
import com.wireguard.android.util.RootShell
fun RootShell.getCurrentWifiName(): String? {
@@ -12,12 +10,3 @@ fun RootShell.getCurrentWifiName(): String? {
)
return response.firstOrNull()
}
@Suppress("DEPRECATION")
fun WifiManager.getCurrentSecurityType(): WifiSecurityType? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
WifiSecurityType.from(connectionInfo.currentSecurityType)
} else {
null
}
}
@@ -9,7 +9,6 @@ sealed class NetworkStatus {
data class Connected(
val wifiSsid: String? = null,
val securityType: WifiSecurityType? = null,
override val wifiConnected: Boolean = false,
override val ethernetConnected: Boolean = false,
override val cellularConnected: Boolean = false,
@@ -1,38 +0,0 @@
package com.zaneschepke.networkmonitor
import android.net.wifi.WifiInfo
enum class WifiSecurityType {
UNKNOWN,
OPEN,
WEP,
WPA2, // WPA and WPA2
WPA3, // WPA3-Personal (SAE)
OWE,
WAPI, // All WAPI_PSK and WAPI_CERT
EAP, // All EAP (covers both WPA3 and others)
PASSPOINT, // All Passpoint versions
DPP;
companion object {
fun from(securityType: Int): WifiSecurityType {
return when (securityType) {
WifiInfo.SECURITY_TYPE_OPEN -> OPEN
WifiInfo.SECURITY_TYPE_WEP -> WEP
WifiInfo.SECURITY_TYPE_PSK -> WPA2
WifiInfo.SECURITY_TYPE_EAP -> EAP
WifiInfo.SECURITY_TYPE_SAE -> WPA3
WifiInfo.SECURITY_TYPE_OWE -> OWE
WifiInfo.SECURITY_TYPE_WAPI_PSK,
WifiInfo.SECURITY_TYPE_WAPI_CERT -> WAPI
WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE -> EAP
WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT -> EAP
WifiInfo.SECURITY_TYPE_PASSPOINT_R1_R2,
WifiInfo.SECURITY_TYPE_PASSPOINT_R3 -> PASSPOINT
WifiInfo.SECURITY_TYPE_DPP -> DPP
WifiInfo.SECURITY_TYPE_UNKNOWN -> UNKNOWN
else -> UNKNOWN
}
}
}
}
+1
View File
@@ -0,0 +1 @@
0