mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11ad494fbb | |||
| 90b006acc5 | |||
| eb7b39c379 | |||
| 0a17593310 | |||
| c0e58125dd | |||
| 3791261f91 | |||
| d1e61be3ae | |||
| afd4fb127f |
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[BUG] - Problem with app"
|
||||||
|
labels: bug
|
||||||
|
assignees: zaneschepke
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- Android Version: [e.g. iOS8.1]
|
||||||
|
- App Version [e.g. 22]
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots (Only if necessary)**
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[FEATURE] - New feature request"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: zaneschepke
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# name of the workflow
|
||||||
|
name: Android CI Tag Deployment
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Signed APK
|
||||||
|
# change to macos because of hilt issues on ubuntu in gradle 8.3
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
KEY_STORE_PATH: ${{ secrets.KEY_STORE_PATH }}
|
||||||
|
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||||
|
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||||
|
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
cache: gradle
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
# Here we need to decode keystore.jks from base64 string and place it
|
||||||
|
# in the folder specified in the release signing configuration
|
||||||
|
- name: Decode Keystore
|
||||||
|
id: decode_keystore
|
||||||
|
uses: timheuer/base64-to-file@v1.2
|
||||||
|
with:
|
||||||
|
fileName: 'android_keystore.jks'
|
||||||
|
fileDir: ${{ github.workspace }}/app/keystore/
|
||||||
|
encodedString: ${{ secrets.KEYSTORE }}
|
||||||
|
|
||||||
|
- name: Create service_account.json
|
||||||
|
id: createServiceAccount
|
||||||
|
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||||
|
|
||||||
|
# Build and sign APK ("-x test" argument is used to skip tests)
|
||||||
|
# add fdroid flavor for apk upload
|
||||||
|
- name: Build Fdroid Release APK
|
||||||
|
run: ./gradlew :app:assembleFdroidRelease -x test
|
||||||
|
|
||||||
|
# get fdroid flavor release apk path
|
||||||
|
- name: Get apk path
|
||||||
|
id: apk-path
|
||||||
|
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Save the APK after the Build job is complete to publish it as a Github release in the next job
|
||||||
|
- name: Upload APK
|
||||||
|
uses: actions/upload-artifact@v3.1.2
|
||||||
|
with:
|
||||||
|
name: wgtunnel
|
||||||
|
path: ${{ steps.apk-path.outputs.path }}
|
||||||
|
- name: Download APK from build
|
||||||
|
uses: actions/download-artifact@v1
|
||||||
|
with:
|
||||||
|
name: wgtunnel
|
||||||
|
- name: Create Release with Fastlane changelog notes
|
||||||
|
id: create_release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
# fix hardcode changelog file name
|
||||||
|
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/32200.txt
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: Release ${{ github.ref_name }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||||
|
- name: Deploy with fastlane
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Distribute app to Beta track 🚀
|
||||||
|
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
||||||
|
|
||||||
@@ -69,3 +69,5 @@ lint/tmp/
|
|||||||
# App Specific cases
|
# App Specific cases
|
||||||
app/release/output.json
|
app/release/output.json
|
||||||
.idea/codeStyles/
|
.idea/codeStyles/
|
||||||
|
# where we keep our signing secrets locally
|
||||||
|
app/signing.properties
|
||||||
|
|||||||
+47
-12
@@ -1,3 +1,5 @@
|
|||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
@@ -7,15 +9,15 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.zaneschepke.wireguardautotunnel"
|
namespace = Constants.APP_ID
|
||||||
compileSdk = 34
|
compileSdk = Constants.TARGET_SDK
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.zaneschepke.wireguardautotunnel"
|
applicationId = Constants.APP_ID
|
||||||
minSdk = 26
|
minSdk = Constants.MIN_SDK
|
||||||
targetSdk = 34
|
targetSdk = Constants.TARGET_SDK
|
||||||
versionCode = 32000
|
versionCode = Constants.VERSION_CODE
|
||||||
versionName = "3.2.0"
|
versionName = Constants.VERSION_NAME
|
||||||
|
|
||||||
ksp {
|
ksp {
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
@@ -29,7 +31,38 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create(Constants.RELEASE) {
|
||||||
|
val properties = Properties().apply {
|
||||||
|
//created local file for signing details
|
||||||
|
try {
|
||||||
|
load(file("signing.properties").reader())
|
||||||
|
} catch (_ : Exception) {
|
||||||
|
load(file("signing_template.properties").reader())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//try to get secrets from env first for pipeline build, then properties file for local build
|
||||||
|
storeFile = file(System.getenv().getOrDefault(Constants.KEY_STORE_PATH_VAR, properties.getProperty(Constants.KEY_STORE_PATH_VAR)))
|
||||||
|
storePassword = System.getenv().getOrDefault(Constants.STORE_PASS_VAR, properties.getProperty(Constants.STORE_PASS_VAR))
|
||||||
|
keyAlias = System.getenv().getOrDefault(Constants.KEY_ALIAS_VAR, properties.getProperty(Constants.KEY_ALIAS_VAR))
|
||||||
|
keyPassword = System.getenv().getOrDefault(Constants.KEY_PASS_VAR, properties.getProperty(Constants.KEY_PASS_VAR))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
//don't strip
|
||||||
|
packaging.jniLibs.keepDebugSymbols.addAll(listOf("libwg-go.so", "libwg-quick.so", "libwg.so"))
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
isDebuggable = false
|
isDebuggable = false
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
@@ -38,19 +71,20 @@ android {
|
|||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
|
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
isDebuggable = true
|
isDebuggable = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flavorDimensions.add("type")
|
flavorDimensions.add(Constants.TYPE)
|
||||||
productFlavors {
|
productFlavors {
|
||||||
create("fdroid") {
|
create("fdroid") {
|
||||||
dimension = "type"
|
dimension = Constants.TYPE
|
||||||
proguardFile("fdroid-rules.pro")
|
proguardFile("fdroid-rules.pro")
|
||||||
}
|
}
|
||||||
create("general") {
|
create("general") {
|
||||||
dimension = "type"
|
dimension = Constants.TYPE
|
||||||
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle))
|
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle))
|
||||||
{
|
{
|
||||||
apply(plugin = "com.google.gms.google-services")
|
apply(plugin = "com.google.gms.google-services")
|
||||||
@@ -61,9 +95,10 @@ android {
|
|||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "17"
|
jvmTarget = Constants.JVM_TARGET
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
@@ -105,6 +140,7 @@ dependencies {
|
|||||||
|
|
||||||
//wg
|
//wg
|
||||||
implementation(libs.tunnel)
|
implementation(libs.tunnel)
|
||||||
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
|
|
||||||
//logging
|
//logging
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
@@ -121,7 +157,6 @@ dependencies {
|
|||||||
implementation(libs.accompanist.systemuicontroller)
|
implementation(libs.accompanist.systemuicontroller)
|
||||||
implementation(libs.accompanist.permissions)
|
implementation(libs.accompanist.permissions)
|
||||||
implementation(libs.accompanist.flowlayout)
|
implementation(libs.accompanist.flowlayout)
|
||||||
implementation(libs.accompanist.navigation.animation)
|
|
||||||
implementation(libs.accompanist.drawablepainter)
|
implementation(libs.accompanist.drawablepainter)
|
||||||
|
|
||||||
//room
|
//room
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
SIGNING_STORE_PASSWORD=
|
||||||
|
SIGNING_KEY_ALIAS=
|
||||||
|
SIGNING_KEY_PASSWORD=
|
||||||
|
KEY_STORE_PATH=/
|
||||||
@@ -4,7 +4,7 @@ object Constants {
|
|||||||
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
const val MANUAL_TUNNEL_CONFIG_ID = "0"
|
||||||
const val WATCHER_SERVICE_WAKE_LOCK_TIMEOUT = 10*60*1000L /*10 minute*/
|
const val WATCHER_SERVICE_WAKE_LOCK_TIMEOUT = 10*60*1000L /*10 minute*/
|
||||||
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
|
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L
|
||||||
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L
|
const val VPN_STATISTIC_CHECK_INTERVAL = 1000L
|
||||||
const val TOGGLE_TUNNEL_DELAY = 500L
|
const val TOGGLE_TUNNEL_DELAY = 500L
|
||||||
const val FADE_IN_ANIMATION_DURATION = 1000
|
const val FADE_IN_ANIMATION_DURATION = 1000
|
||||||
const val SLIDE_IN_ANIMATION_DURATION = 500
|
const val SLIDE_IN_ANIMATION_DURATION = 500
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.text.DecimalFormat
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
@@ -22,3 +24,8 @@ fun BroadcastReceiver.goAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BigDecimal.toThreeDecimalPlaceString() : String {
|
||||||
|
val df = DecimalFormat("#.###")
|
||||||
|
return df.format(this)
|
||||||
|
}
|
||||||
|
|||||||
+2
-1
@@ -106,7 +106,8 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
val notification = notificationService.createNotification(
|
val notification = notificationService.createNotification(
|
||||||
channelId = getString(R.string.watcher_channel_id),
|
channelId = getString(R.string.watcher_channel_id),
|
||||||
channelName = getString(R.string.watcher_channel_name),
|
channelName = getString(R.string.watcher_channel_name),
|
||||||
description = getString(R.string.watcher_notification_text)
|
description = getString(R.string.watcher_notification_text),
|
||||||
|
vibration = false
|
||||||
)
|
)
|
||||||
super.startForeground(foregroundId, notification)
|
super.startForeground(foregroundId, notification)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -120,6 +120,7 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
channelName = getString(R.string.vpn_channel_name),
|
channelName = getString(R.string.vpn_channel_name),
|
||||||
title = getString(R.string.tunnel_start_title),
|
title = getString(R.string.tunnel_start_title),
|
||||||
onGoing = false,
|
onGoing = false,
|
||||||
|
vibration = false,
|
||||||
showTimestamp = true,
|
showTimestamp = true,
|
||||||
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
|
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
|
||||||
)
|
)
|
||||||
@@ -132,6 +133,7 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
channelName = getString(R.string.vpn_channel_name),
|
channelName = getString(R.string.vpn_channel_name),
|
||||||
title = getString(R.string.vpn_starting),
|
title = getString(R.string.vpn_starting),
|
||||||
onGoing = false,
|
onGoing = false,
|
||||||
|
vibration = false,
|
||||||
showTimestamp = true,
|
showTimestamp = true,
|
||||||
description = getString(R.string.attempt_connection)
|
description = getString(R.string.attempt_connection)
|
||||||
)
|
)
|
||||||
@@ -147,6 +149,7 @@ class WireGuardTunnelService : ForegroundService() {
|
|||||||
actionText = getString(R.string.restart),
|
actionText = getString(R.string.restart),
|
||||||
title = getString(R.string.vpn_connection_failed),
|
title = getString(R.string.vpn_connection_failed),
|
||||||
onGoing = false,
|
onGoing = false,
|
||||||
|
vibration = true,
|
||||||
showTimestamp = true,
|
showTimestamp = true,
|
||||||
description = message
|
description = message
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-4
@@ -4,7 +4,6 @@ import android.net.NetworkCapabilities
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface NetworkService<T> {
|
interface NetworkService<T> {
|
||||||
fun getNetworkName(networkCapabilities: NetworkCapabilities) : String?
|
fun getNetworkName(networkCapabilities: NetworkCapabilities): String?
|
||||||
val networkStatus : Flow<NetworkStatus>
|
val networkStatus: Flow<NetworkStatus>
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ interface NotificationService {
|
|||||||
description: String,
|
description: String,
|
||||||
showTimestamp : Boolean = false,
|
showTimestamp : Boolean = false,
|
||||||
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
||||||
vibration: Boolean = true,
|
vibration: Boolean = false,
|
||||||
onGoing: Boolean = true,
|
onGoing: Boolean = true,
|
||||||
lights: Boolean = true
|
lights: Boolean = true
|
||||||
): Notification
|
): Notification
|
||||||
|
|||||||
+16
-12
@@ -110,20 +110,24 @@ class TunnelControlTile : TileService() {
|
|||||||
|
|
||||||
private suspend fun updateTileState() {
|
private suspend fun updateTileState() {
|
||||||
vpnService.state.collect {
|
vpnService.state.collect {
|
||||||
when(it) {
|
try {
|
||||||
Tunnel.State.UP -> {
|
when(it) {
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
Tunnel.State.UP -> {
|
||||||
}
|
qsTile.state = Tile.STATE_ACTIVE
|
||||||
Tunnel.State.DOWN -> {
|
}
|
||||||
qsTile.state = Tile.STATE_INACTIVE
|
Tunnel.State.DOWN -> {
|
||||||
}
|
qsTile.state = Tile.STATE_INACTIVE
|
||||||
else -> {
|
}
|
||||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
else -> {
|
||||||
|
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
val config = determineTileTunnel()
|
||||||
|
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
|
||||||
|
qsTile.updateTile()
|
||||||
|
} catch (e : Exception) {
|
||||||
|
Timber.e("Unable to update tile state")
|
||||||
}
|
}
|
||||||
val config = determineTileTunnel()
|
|
||||||
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
|
|
||||||
qsTile.updateTile()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-4
@@ -23,8 +23,7 @@ import timber.log.Timber
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
class WireGuardTunnel @Inject constructor(private val backend : Backend) : VpnService {
|
||||||
) : VpnService {
|
|
||||||
|
|
||||||
private val _tunnelName = MutableStateFlow("")
|
private val _tunnelName = MutableStateFlow("")
|
||||||
override val tunnelName get() = _tunnelName.asStateFlow()
|
override val tunnelName get() = _tunnelName.asStateFlow()
|
||||||
@@ -115,11 +114,11 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
|||||||
_handshakeStatus.emit(HandshakeStatus.NOT_STARTED)
|
_handshakeStatus.emit(HandshakeStatus.NOT_STARTED)
|
||||||
}
|
}
|
||||||
if(neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
if(neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
||||||
neverHadHandshakeCounter += 10
|
neverHadHandshakeCounter += (1 * Constants.VPN_STATISTIC_CHECK_INTERVAL/1000).toInt()
|
||||||
}
|
}
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
if(NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) >= HandshakeStatus.UNHEALTHY_TIME_LIMIT_SEC) {
|
if((NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) ?: 0L) >= HandshakeStatus.UNHEALTHY_TIME_LIMIT_SEC) {
|
||||||
_handshakeStatus.emit(HandshakeStatus.UNHEALTHY)
|
_handshakeStatus.emit(HandshakeStatus.UNHEALTHY)
|
||||||
} else {
|
} else {
|
||||||
_handshakeStatus.emit(HandshakeStatus.HEALTHY)
|
_handshakeStatus.emit(HandshakeStatus.HEALTHY)
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import com.google.accompanist.navigation.animation.composable
|
import androidx.navigation.compose.composable
|
||||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
@@ -47,7 +47,6 @@ import com.zaneschepke.wireguardautotunnel.ui.common.PermissionRequestFailedScre
|
|||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.detail.DetailScreen
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.SettingsScreen
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||||
@@ -67,7 +66,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
val navController = rememberAnimatedNavController()
|
val navController = rememberNavController()
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
WireguardAutoTunnelTheme {
|
WireguardAutoTunnelTheme {
|
||||||
@@ -172,7 +171,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return@Scaffold
|
return@Scaffold
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedNavHost(navController, startDestination = Routes.Main.name) {
|
NavHost(navController, startDestination = Routes.Main.name) {
|
||||||
composable(Routes.Main.name, enterTransition = {
|
composable(Routes.Main.name, enterTransition = {
|
||||||
when (initialState.destination.route) {
|
when (initialState.destination.route) {
|
||||||
Routes.Settings.name, Routes.Support.name ->
|
Routes.Settings.name, Routes.Support.name ->
|
||||||
@@ -230,14 +229,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val id = it.arguments?.getString("id")
|
val id = it.arguments?.getString("id")
|
||||||
if(!id.isNullOrBlank()) {
|
if(!id.isNullOrBlank()) {
|
||||||
ConfigScreen(navController = navController, id = id, showSnackbarMessage = { message -> showSnackBarMessage(message) }, focusRequester = focusRequester)}
|
ConfigScreen(navController = navController, id = id, showSnackbarMessage = { message -> showSnackBarMessage(message) }, focusRequester = focusRequester)}
|
||||||
}
|
|
||||||
composable("${Routes.Detail.name}/{id}", enterTransition = {
|
|
||||||
fadeIn(animationSpec = tween(Constants.FADE_IN_ANIMATION_DURATION))
|
|
||||||
}) {
|
|
||||||
val id = it.arguments?.getString("id")
|
|
||||||
if(!id.isNullOrBlank()) {
|
|
||||||
DetailScreen(padding = padding, focusRequester = focusRequester, id = id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ enum class Routes {
|
|||||||
Main,
|
Main,
|
||||||
Settings,
|
Settings,
|
||||||
Support,
|
Support,
|
||||||
Config,
|
Config;
|
||||||
Detail;
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,23 +1,36 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.wireguard.android.backend.Statistics
|
||||||
|
import com.zaneschepke.wireguardautotunnel.toThreeDecimalPlaceString
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RowListItem(icon : @Composable() () -> Unit, text : String, onHold : () -> Unit, onClick: () -> Unit, rowButton : @Composable() () -> Unit ) {
|
fun RowListItem(icon : @Composable () -> Unit, text : String, onHold : () -> Unit,
|
||||||
|
onClick: () -> Unit, rowButton : @Composable () -> Unit,
|
||||||
|
expanded : Boolean, statistics: Statistics?
|
||||||
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.animateContentSize()
|
||||||
|
.clip(RoundedCornerShape(30.dp))
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
onClick()
|
onClick()
|
||||||
@@ -27,19 +40,45 @@ fun RowListItem(icon : @Composable() () -> Unit, text : String, onHold : () -> U
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Row(
|
Column {
|
||||||
modifier = Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.padding(14.dp),
|
.fillMaxWidth()
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
.padding(horizontal = 14.dp, vertical = 5.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
Row(verticalAlignment = Alignment.CenterVertically,) {
|
) {
|
||||||
icon()
|
Row(verticalAlignment = Alignment.CenterVertically,) {
|
||||||
Text(text)
|
icon()
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
rowButton()
|
||||||
|
}
|
||||||
|
if(expanded) {
|
||||||
|
statistics?.peers()?.forEach {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(end = 10.dp, bottom = 10.dp, start = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis
|
||||||
|
val peerTx = statistics.peer(it)!!.txBytes
|
||||||
|
val peerRx = statistics.peer(it)!!.rxBytes
|
||||||
|
val peerId = it.toBase64().subSequence(0,3).toString() + "***"
|
||||||
|
val handshakeSec = NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
|
||||||
|
val handshake = if(handshakeSec == null) "never" else "$handshakeSec secs ago"
|
||||||
|
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString()
|
||||||
|
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString()
|
||||||
|
val fontSize = 9.sp
|
||||||
|
Text("peer: $peerId", fontSize = fontSize)
|
||||||
|
Text("handshake: $handshake", fontSize = fontSize)
|
||||||
|
Text("tx: $peerTxMB MB", fontSize = fontSize)
|
||||||
|
Text("rx: $peerRxMB MB", fontSize = fontSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rowButton()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-161
@@ -1,161 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.detail
|
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.focusGroup
|
|
||||||
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.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
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.focus.FocusRequester
|
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
|
||||||
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.font.FontStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
fun DetailScreen(
|
|
||||||
viewModel: DetailViewModel = hiltViewModel(),
|
|
||||||
focusRequester: FocusRequester,
|
|
||||||
padding: PaddingValues,
|
|
||||||
id : String
|
|
||||||
) {
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
|
||||||
val tunnelStats by viewModel.tunnelStats.collectAsStateWithLifecycle(null)
|
|
||||||
val tunnel by viewModel.tunnel.collectAsStateWithLifecycle(null)
|
|
||||||
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle()
|
|
||||||
val lastHandshake by viewModel.lastHandshake.collectAsStateWithLifecycle(emptyMap())
|
|
||||||
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
viewModel.emitConfig(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(null != tunnel) {
|
|
||||||
val interfaceKey = tunnel?.`interface`?.keyPair?.publicKey?.toBase64().toString()
|
|
||||||
val addresses = tunnel?.`interface`?.addresses!!.joinToString()
|
|
||||||
val dnsServers = tunnel?.`interface`?.dnsServers!!.joinToString()
|
|
||||||
val optionalMtu = tunnel?.`interface`?.mtu
|
|
||||||
val mtu = if(optionalMtu?.isPresent == true) optionalMtu.get().toString() else stringResource(
|
|
||||||
id = R.string.none
|
|
||||||
)
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.Start,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.fillMaxHeight(if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) 4/5f else 1f)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.focusRequester(focusRequester)
|
|
||||||
.padding(padding)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 20.dp, vertical = 7.dp).focusGroup(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1f, true)) {
|
|
||||||
Text(stringResource(R.string.config_interface), fontWeight = FontWeight.Bold, fontSize = 20.sp)
|
|
||||||
Text(stringResource(R.string.name), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = tunnelName, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(tunnelName))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = interfaceKey, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(interfaceKey))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.addresses), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = addresses, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(addresses))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.dns_servers), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = dnsServers, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(dnsServers))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.mtu), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = mtu, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(mtu))
|
|
||||||
})
|
|
||||||
Box(modifier = Modifier.padding(10.dp))
|
|
||||||
tunnel?.peers?.forEach{
|
|
||||||
val peerKey = it.publicKey.toBase64()
|
|
||||||
val allowedIps = it.allowedIps.joinToString()
|
|
||||||
val endpoint = if(it.endpoint.isPresent) it.endpoint.get().toString() else stringResource(
|
|
||||||
id = R.string.none
|
|
||||||
)
|
|
||||||
Text(stringResource(R.string.peer), fontWeight = FontWeight.Bold, fontSize = 20.sp)
|
|
||||||
Text(stringResource(R.string.public_key), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = peerKey, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(peerKey))
|
|
||||||
})
|
|
||||||
Text(stringResource(id = R.string.allowed_ips), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = allowedIps, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(allowedIps))
|
|
||||||
})
|
|
||||||
Text(stringResource(R.string.endpoint), fontStyle = FontStyle.Italic)
|
|
||||||
Text(text = endpoint, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(endpoint))
|
|
||||||
})
|
|
||||||
if (tunnelStats != null) {
|
|
||||||
val totalRx = tunnelStats?.totalRx() ?: 0
|
|
||||||
val totalTx = tunnelStats?.totalTx() ?: 0
|
|
||||||
if((totalRx + totalTx != 0L)) {
|
|
||||||
val rxKB = NumberUtils.bytesToKB(tunnelStats!!.totalRx())
|
|
||||||
val txKB = NumberUtils.bytesToKB(tunnelStats!!.totalTx())
|
|
||||||
Text(stringResource(R.string.transfer), fontStyle = FontStyle.Italic)
|
|
||||||
val transfer = "rx: ${NumberUtils.formatDecimalTwoPlaces(rxKB)} KB tx: ${NumberUtils.formatDecimalTwoPlaces(txKB)} KB"
|
|
||||||
Text(transfer, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(transfer))})
|
|
||||||
Text(stringResource(R.string.last_handshake), fontStyle = FontStyle.Italic)
|
|
||||||
val handshakeEpoch = lastHandshake[it.publicKey]
|
|
||||||
if(handshakeEpoch != null) {
|
|
||||||
if(handshakeEpoch == 0L) {
|
|
||||||
Text(stringResource(id = R.string.never), modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(context.getString(R.string.never)))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
val time = Instant.ofEpochMilli(handshakeEpoch)
|
|
||||||
val duration = "${Duration.between(time, Instant.now()).seconds} seconds ago"
|
|
||||||
Text(duration, modifier = Modifier.clickable {
|
|
||||||
clipboardManager.setText(AnnotatedString(duration))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-46
@@ -1,46 +0,0 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.detail
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.wireguard.config.Config
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.TunnelConfigDao
|
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.model.TunnelConfig
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class DetailViewModel @Inject constructor(private val tunnelRepo : TunnelConfigDao, private val vpnService : VpnService
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val _tunnel = MutableStateFlow<Config?>(null)
|
|
||||||
val tunnel get() = _tunnel.asStateFlow()
|
|
||||||
|
|
||||||
private val _tunnelName = MutableStateFlow("")
|
|
||||||
val tunnelName = _tunnelName.asStateFlow()
|
|
||||||
val tunnelStats get() = vpnService.statistics
|
|
||||||
val lastHandshake get() = vpnService.lastHandshake
|
|
||||||
|
|
||||||
private suspend fun getTunnelConfigById(id: String): TunnelConfig? {
|
|
||||||
return try {
|
|
||||||
tunnelRepo.getById(id.toLong())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e.message)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun emitConfig(id: String) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
val tunnelConfig = getTunnelConfigById(id)
|
|
||||||
if(tunnelConfig != null) {
|
|
||||||
_tunnelName.emit(tunnelConfig.name)
|
|
||||||
_tunnel.emit(TunnelConfig.configFromQuick(tunnelConfig.wgQuick))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+44
-23
@@ -117,6 +117,7 @@ fun MainScreen(
|
|||||||
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
|
val state by viewModel.state.collectAsStateWithLifecycle(Tunnel.State.DOWN)
|
||||||
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
|
val tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
|
||||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
||||||
|
val statistics by viewModel.statistics.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
// Nested scroll for control FAB
|
// Nested scroll for control FAB
|
||||||
val nestedScrollConnection = remember {
|
val nestedScrollConnection = remember {
|
||||||
@@ -171,8 +172,14 @@ fun MainScreen(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
viewModel.onTunnelQrResult(it.contents)
|
viewModel.onTunnelQrResult(it.contents)
|
||||||
} catch (e: WgTunnelException) {
|
} catch (e: Exception) {
|
||||||
showSnackbarMessage(e.message)
|
when(e) {
|
||||||
|
is WgTunnelException -> {
|
||||||
|
showSnackbarMessage(e.message)
|
||||||
|
} else -> {
|
||||||
|
showSnackbarMessage("No QR code scanned")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,9 +236,12 @@ fun MainScreen(
|
|||||||
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
val hoverColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
modifier = Modifier.padding(bottom = 90.dp).onFocusChanged {
|
modifier = Modifier
|
||||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
.padding(bottom = 90.dp)
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor }
|
.onFocusChanged {
|
||||||
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
,
|
,
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -275,7 +285,7 @@ fun MainScreen(
|
|||||||
showBottomSheet = false
|
showBottomSheet = false
|
||||||
try {
|
try {
|
||||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||||
} catch (e : Exception) {
|
} catch (e: Exception) {
|
||||||
showSnackbarMessage(e.message!!)
|
showSnackbarMessage(e.message!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,17 +372,24 @@ fun MainScreen(
|
|||||||
HandshakeStatus.NEVER_CONNECTED -> brickRed
|
HandshakeStatus.NEVER_CONNECTED -> brickRed
|
||||||
} else {Color.Gray})
|
} else {Color.Gray})
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val expanded = remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
RowListItem(icon = {
|
RowListItem(icon = {
|
||||||
if (settings.isTunnelConfigDefault(tunnel))
|
if (settings.isTunnelConfigDefault(tunnel))
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Star, stringResource(R.string.status),
|
Icons.Rounded.Star, stringResource(R.string.status),
|
||||||
tint = leadingIconColor,
|
tint = leadingIconColor,
|
||||||
modifier = Modifier.padding(end = 10.dp).size(20.dp)
|
modifier = Modifier
|
||||||
|
.padding(end = 10.dp)
|
||||||
|
.size(20.dp)
|
||||||
)
|
)
|
||||||
else Icon(
|
else Icon(
|
||||||
Icons.Rounded.Circle, stringResource(R.string.status),
|
Icons.Rounded.Circle, stringResource(R.string.status),
|
||||||
tint = leadingIconColor,
|
tint = leadingIconColor,
|
||||||
modifier = Modifier.padding(end = 15.dp).size(15.dp)
|
modifier = Modifier
|
||||||
|
.padding(end = 15.dp)
|
||||||
|
.size(15.dp)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = tunnel.name,
|
text = tunnel.name,
|
||||||
@@ -386,12 +403,16 @@ fun MainScreen(
|
|||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
|
if(state == Tunnel.State.UP && (tunnelName == tunnel.name) ) {
|
||||||
|
expanded.value = !expanded.value
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedTunnel = tunnel
|
selectedTunnel = tunnel
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
statistics = statistics,
|
||||||
|
expanded = expanded.value,
|
||||||
rowButton = {
|
rowButton = {
|
||||||
if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
Row {
|
Row {
|
||||||
@@ -419,6 +440,15 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@Composable
|
||||||
|
fun TunnelSwitch() = Switch(
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
if(!checked) expanded.value = false
|
||||||
|
onTunnelToggle(checked, tunnel)
|
||||||
|
}
|
||||||
|
)
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
Row {
|
Row {
|
||||||
if(!settings.isTunnelConfigDefault(tunnel)) {
|
if(!settings.isTunnelConfigDefault(tunnel)) {
|
||||||
@@ -433,7 +463,9 @@ fun MainScreen(
|
|||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
onClick = {
|
onClick = {
|
||||||
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
|
if(state == Tunnel.State.UP && (tunnelName == tunnel.name) ) {
|
||||||
|
expanded.value = !expanded.value
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
Icon(Icons.Rounded.Info, stringResource(R.string.info))
|
||||||
}
|
}
|
||||||
@@ -469,21 +501,10 @@ fun MainScreen(
|
|||||||
stringResource(id = R.string.delete)
|
stringResource(id = R.string.delete)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Switch(
|
TunnelSwitch()
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
|
||||||
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
|
|
||||||
onCheckedChange = { checked ->
|
|
||||||
onTunnelToggle(checked, tunnel)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Switch(
|
TunnelSwitch()
|
||||||
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
|
|
||||||
onCheckedChange = { checked ->
|
|
||||||
onTunnelToggle(checked, tunnel)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class MainViewModel @Inject constructor(
|
|||||||
val tunnelName get() = vpnService.tunnelName
|
val tunnelName get() = vpnService.tunnelName
|
||||||
private val _settings = MutableStateFlow(Settings())
|
private val _settings = MutableStateFlow(Settings())
|
||||||
val settings get() = _settings.asStateFlow()
|
val settings get() = _settings.asStateFlow()
|
||||||
|
val statistics get() = vpnService.statistics
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
|||||||
+72
-56
@@ -99,7 +99,7 @@ fun SettingsScreen(
|
|||||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
var currentText by remember { mutableStateOf("") }
|
var currentText by remember { mutableStateOf("") }
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
var didShowLocationDisclaimer by remember { mutableStateOf(false) }
|
var isLocationDisclaimerNeeded by remember { mutableStateOf(true) }
|
||||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||||
var didExportFiles by remember { mutableStateOf(false) }
|
var didExportFiles by remember { mutableStateOf(false) }
|
||||||
@@ -141,6 +141,7 @@ fun SettingsScreen(
|
|||||||
return(isBackgroundLocationGranted && fineLocationState.status.isGranted && !viewModel.isLocationServicesNeeded())
|
return(isBackgroundLocationGranted && fineLocationState.status.isGranted && !viewModel.isLocationServicesNeeded())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun openSettings() {
|
fun openSettings() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val intentSettings =
|
val intentSettings =
|
||||||
@@ -156,62 +157,75 @@ fun SettingsScreen(
|
|||||||
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||||
if(!backgroundLocationState.status.isGranted) {
|
if(!backgroundLocationState.status.isGranted) {
|
||||||
isBackgroundLocationGranted = false
|
isBackgroundLocationGranted = false
|
||||||
if(!didShowLocationDisclaimer) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.padding(padding)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Rounded.LocationOff,
|
|
||||||
contentDescription = stringResource(id = R.string.map),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(30.dp)
|
|
||||||
.size(128.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.prominent_background_location_title),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(30.dp),
|
|
||||||
fontSize = 20.sp
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.prominent_background_location_message),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(30.dp),
|
|
||||||
fontSize = 15.sp
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(10.dp) else Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(30.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
TextButton(onClick = {
|
|
||||||
didShowLocationDisclaimer = true
|
|
||||||
}) {
|
|
||||||
Text(stringResource(id = R.string.no_thanks))
|
|
||||||
}
|
|
||||||
TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = {
|
|
||||||
openSettings()
|
|
||||||
}) {
|
|
||||||
Text(stringResource(id = R.string.turn_on))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
isLocationDisclaimerNeeded = false
|
||||||
isBackgroundLocationGranted = true
|
isBackgroundLocationGranted = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||||
|
if(!fineLocationState.status.isGranted) {
|
||||||
|
isBackgroundLocationGranted = false
|
||||||
|
} else {
|
||||||
|
isLocationDisclaimerNeeded = false
|
||||||
|
isBackgroundLocationGranted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isLocationDisclaimerNeeded) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.padding(padding)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.LocationOff,
|
||||||
|
contentDescription = stringResource(id = R.string.map),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(30.dp)
|
||||||
|
.size(128.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.prominent_background_location_title),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(30.dp),
|
||||||
|
fontSize = 20.sp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.prominent_background_location_message),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(30.dp),
|
||||||
|
fontSize = 15.sp
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp) else Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(30.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
TextButton(onClick = {
|
||||||
|
isLocationDisclaimerNeeded = false
|
||||||
|
}) {
|
||||||
|
Text(stringResource(id = R.string.no_thanks))
|
||||||
|
}
|
||||||
|
TextButton(modifier = Modifier.focusRequester(focusRequester), onClick = {
|
||||||
|
openSettings()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(id = R.string.turn_on))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if(showAuthPrompt) {
|
if(showAuthPrompt) {
|
||||||
AuthorizationPrompt(onSuccess = {
|
AuthorizationPrompt(onSuccess = {
|
||||||
showAuthPrompt = false
|
showAuthPrompt = false
|
||||||
@@ -262,8 +276,8 @@ fun SettingsScreen(
|
|||||||
modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context))
|
modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context))
|
||||||
Modifier
|
Modifier
|
||||||
.height(IntrinsicSize.Min)
|
.height(IntrinsicSize.Min)
|
||||||
.fillMaxWidth(fillMaxWidth)
|
.fillMaxWidth(fillMaxWidth).padding(top = 10.dp)
|
||||||
else Modifier.fillMaxWidth(fillMaxWidth)).padding(top = 60.dp, bottom = 25.dp)
|
else Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp)).padding(bottom = 25.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
@@ -276,8 +290,10 @@ fun SettingsScreen(
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
|
modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
|
||||||
)
|
)
|
||||||
|
val focus = Modifier.focusRequester(focusRequester)
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.padding(screenPadding),
|
modifier = (if(trustedSSIDs.isEmpty()) Modifier else
|
||||||
|
focus).padding(screenPadding),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalArrangement = Arrangement.SpaceEvenly
|
verticalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
@@ -302,7 +318,7 @@ fun SettingsScreen(
|
|||||||
value = currentText,
|
value = currentText,
|
||||||
onValueChange = { currentText = it },
|
onValueChange = { currentText = it },
|
||||||
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
label = { Text(stringResource(R.string.add_trusted_ssid)) },
|
||||||
modifier = Modifier.padding(start = screenPadding, top = 5.dp).focusRequester(focusRequester).onFocusChanged {
|
modifier = (if(trustedSSIDs.isEmpty()) focus else Modifier).padding(start = screenPadding, top = 5.dp).onFocusChanged {
|
||||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.util
|
package com.zaneschepke.wireguardautotunnel.util
|
||||||
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.text.DecimalFormat
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
object NumberUtils {
|
object NumberUtils {
|
||||||
|
|
||||||
private const val BYTES_IN_KB = 1024L
|
private const val BYTES_IN_KB = 1024.0
|
||||||
|
private val BYTES_IN_MB = BYTES_IN_KB.pow(2.0)
|
||||||
private val keyValidationRegex = """^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=${'$'}""".toRegex()
|
private val keyValidationRegex = """^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=${'$'}""".toRegex()
|
||||||
|
|
||||||
fun bytesToKB(bytes : Long) : BigDecimal {
|
fun bytesToMB(bytes : Long) : BigDecimal {
|
||||||
return bytes.toBigDecimal().divide(BYTES_IN_KB.toBigDecimal())
|
return bytes.toBigDecimal().divide(BYTES_IN_MB.toBigDecimal())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isValidKey(key : String) : Boolean {
|
fun isValidKey(key : String) : Boolean {
|
||||||
@@ -22,13 +23,12 @@ object NumberUtils {
|
|||||||
return "tunnel${(Math.random() * 100000).toInt()}"
|
return "tunnel${(Math.random() * 100000).toInt()}"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatDecimalTwoPlaces(bigDecimal: BigDecimal) : String {
|
fun getSecondsBetweenTimestampAndNow(epoch : Long) : Long? {
|
||||||
val df = DecimalFormat("#.##")
|
return if (epoch != 0L) {
|
||||||
return df.format(bigDecimal)
|
val time = Instant.ofEpochMilli(epoch)
|
||||||
}
|
return Duration.between(time, Instant.now()).seconds
|
||||||
|
} else {
|
||||||
fun getSecondsBetweenTimestampAndNow(epoch : Long) : Long {
|
null
|
||||||
val time = Instant.ofEpochMilli(epoch)
|
}
|
||||||
return Duration.between(time, Instant.now()).seconds
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<string name="tunnel_on_ethernet">Tunnel on ethernet</string>
|
<string name="tunnel_on_ethernet">Tunnel on ethernet</string>
|
||||||
<string name="prominent_background_location_message">This feature requires background location permission to enable Wi-Fi SSID monitoring even while the application is closed. For more details, please see the Privacy Policy linked on the Support screen.</string>
|
<string name="prominent_background_location_message">This feature requires background location permission to enable Wi-Fi SSID monitoring even while the application is closed. For more details, please see the Privacy Policy linked on the Support screen.</string>
|
||||||
<string name="prominent_background_location_title">Background Location Disclosure</string>
|
<string name="prominent_background_location_title">Background Location Disclosure</string>
|
||||||
<string name="support_text">Thank you for using WG Tunnel! If you are experiencing issues with the app, please reach out on Discord or create an issue on Github. I will try to address the issue as quickly as possible. Thank you!</string>
|
<string name="support_text">Thank you for using WG Tunnel! If you are experiencing issues with the app, please reach out on Discord or create an issue on GitHub. I will try to address the issue as quickly as possible. Thank you!</string>
|
||||||
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
<string name="trusted_ssid_empty_description">Enter SSID</string>
|
||||||
<string name="trusted_ssid_value_description">Submit SSID</string>
|
<string name="trusted_ssid_value_description">Submit SSID</string>
|
||||||
<string name="config_validation">[Interface]</string>
|
<string name="config_validation">[Interface]</string>
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
<string name="authentication_failed">Authentication failed</string>
|
<string name="authentication_failed">Authentication failed</string>
|
||||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||||
<string name="export_configs">Export configs</string>
|
<string name="export_configs">Export configs</string>
|
||||||
<string name="battery_saver">Battery saver (experimental)</string>
|
<string name="battery_saver">Battery saver (beta)</string>
|
||||||
<string name="location_services_required">Location services required</string>
|
<string name="location_services_required">Location services required</string>
|
||||||
<string name="background_location_required">Background location required</string>
|
<string name="background_location_required">Background location required</string>
|
||||||
<string name="precise_location_required">Precise location required</string>
|
<string name="precise_location_required">Precise location required</string>
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ buildscript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
object Constants {
|
||||||
|
const val VERSION_NAME = "3.2.2"
|
||||||
|
const val JVM_TARGET = "17"
|
||||||
|
const val VERSION_CODE = 32200
|
||||||
|
const val TARGET_SDK = 34
|
||||||
|
const val MIN_SDK = 26
|
||||||
|
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||||
|
const val APP_NAME = "wgtunnel"
|
||||||
|
|
||||||
|
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 TYPE = "type"
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
json_key_file "service_account.json"
|
||||||
|
package_name "com.zaneschepke.wireguardautotunnel"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
default_platform(:android)
|
||||||
|
|
||||||
|
platform :android do
|
||||||
|
|
||||||
|
desc "Deploy a beta version to the Google Play"
|
||||||
|
lane :beta do
|
||||||
|
gradle(task: "clean bundleGeneralRelease")
|
||||||
|
upload_to_play_store(track: 'beta')
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Deploy a new version to the Google Play"
|
||||||
|
lane :production do
|
||||||
|
gradle(task: "clean bundleGeneralRelease")
|
||||||
|
upload_to_play_store
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Enhancements:
|
||||||
|
- Fix < Android 9 permission bug
|
||||||
|
- Other optimizations
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Enhancements:
|
||||||
|
- Add tunnel statistics to main screen
|
||||||
|
- Improve settings screen AndroidTV navigation
|
||||||
|
- Remove notification vibration
|
||||||
|
- Various other bug fixes
|
||||||
+13
-12
@@ -1,43 +1,43 @@
|
|||||||
[versions]
|
[versions]
|
||||||
accompanist = "0.31.2-alpha"
|
accompanist = "0.32.0"
|
||||||
activityCompose = "1.8.0"
|
activityCompose = "1.8.1"
|
||||||
androidx-junit = "1.1.5"
|
androidx-junit = "1.1.5"
|
||||||
appcompat = "1.6.1"
|
appcompat = "1.6.1"
|
||||||
biometricKtx = "1.2.0-alpha05"
|
biometricKtx = "1.2.0-alpha05"
|
||||||
coreGoogleShortcuts = "1.1.0"
|
coreGoogleShortcuts = "1.1.0"
|
||||||
coreKtx = "1.12.0"
|
coreKtx = "1.12.0"
|
||||||
|
desugar_jdk_libs = "2.0.4"
|
||||||
espressoCore = "3.5.1"
|
espressoCore = "3.5.1"
|
||||||
firebase-crashlytics-gradle = "2.9.9"
|
firebase-crashlytics-gradle = "2.9.9"
|
||||||
google-services = "4.4.0"
|
google-services = "4.4.0"
|
||||||
hiltAndroid = "2.48"
|
hiltAndroid = "2.48.1"
|
||||||
hiltNavigationCompose = "1.0.0"
|
hiltNavigationCompose = "1.1.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlinx-serialization-json = "1.5.1"
|
kotlinx-serialization-json = "1.6.0"
|
||||||
lifecycle-runtime-compose = "2.6.2"
|
lifecycle-runtime-compose = "2.6.2"
|
||||||
material-icons-extended = "1.5.4"
|
material-icons-extended = "1.5.4"
|
||||||
material3 = "1.1.2"
|
material3 = "1.1.2"
|
||||||
navigationCompose = "2.7.4"
|
navigationCompose = "2.7.5"
|
||||||
roomVersion = "2.6.0"
|
roomVersion = "2.6.0"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
tunnel = "1.0.20230706"
|
tunnel = "1.0.20230706"
|
||||||
androidGradlePlugin = "8.3.0-alpha06"
|
androidGradlePlugin = "8.2.0-rc03"
|
||||||
kotlin="1.9.10"
|
kotlin="1.9.10"
|
||||||
ksp="1.9.10-1.0.13"
|
ksp="1.9.10-1.0.13"
|
||||||
composeBom="2023.10.01"
|
composeBom="2023.10.01"
|
||||||
firebaseBom="32.4.0"
|
firebaseBom= "32.6.0"
|
||||||
compose="1.5.4"
|
compose="1.5.4"
|
||||||
crashlytics="18.5.0"
|
crashlytics= "18.6.0"
|
||||||
analytics="21.4.0"
|
analytics="21.5.0"
|
||||||
composeCompiler="1.5.3"
|
composeCompiler="1.5.3"
|
||||||
zxingAndroidEmbedded = "4.3.0"
|
zxingAndroidEmbedded = "4.3.0"
|
||||||
zxingCore = "3.4.1"
|
zxingCore = "3.5.2"
|
||||||
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# accompanist
|
# accompanist
|
||||||
accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" }
|
accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" }
|
||||||
accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" }
|
accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" }
|
||||||
accompanist-navigation-animation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanist" }
|
|
||||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
||||||
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
|
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ androidx-compose-ui-tooling-preview = { module="androidx.compose.ui:ui-tooling-p
|
|||||||
androidx-compose-ui = { module="androidx.compose.ui:ui", version.ref="compose" }
|
androidx-compose-ui = { module="androidx.compose.ui:ui", version.ref="compose" }
|
||||||
|
|
||||||
#hilt
|
#hilt
|
||||||
|
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
|
||||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
|
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
|
||||||
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }
|
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
#Wed Oct 11 22:39:21 EDT 2023
|
#Wed Oct 11 22:39:21 EDT 2023
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
Reference in New Issue
Block a user