feat: add statistics to main screen
Removed Details screen and moved tunnel statistics to be shown on expansion of a tunnel config row Fixes bug where not scanning a QR code could cause app to crash when navigating back from camera view Remove vibrations from notifications Improve navigation of settings screen on AndroidTV
This commit is contained in:
parent
90b006acc5
commit
11ad494fbb
|
@ -70,21 +70,18 @@ jobs:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
# fix hardcode changelog file name
|
# fix hardcode changelog file name
|
||||||
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/32100.txt
|
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/32200.txt
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
name: Release ${{ github.ref_name }}
|
name: Release ${{ github.ref_name }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
|
||||||
deploy:
|
- name: Deploy with fastlane
|
||||||
name: Deploy with fastlane
|
uses: ruby/setup-ruby@v1
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: ruby/setup-ruby@v1
|
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.2' # Not needed with a .ruby-version file
|
ruby-version: '3.2' # Not needed with a .ruby-version file
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
||||||
- name: Distribute app to Beta track 🚀
|
- name: Distribute app to Beta track 🚀
|
||||||
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("release") {
|
create(Constants.RELEASE) {
|
||||||
val properties = Properties().apply {
|
val properties = Properties().apply {
|
||||||
//created local file for signing details
|
//created local file for signing details
|
||||||
try {
|
try {
|
||||||
|
@ -59,7 +59,7 @@ android {
|
||||||
variant.outputs
|
variant.outputs
|
||||||
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
||||||
.forEach { output ->
|
.forEach { output ->
|
||||||
val outputFileName = "wgtunnel-${variant.flavorName}-${variant.buildType.name}-${variant.versionName}.apk"
|
val outputFileName = "${Constants.APP_NAME}-${variant.flavorName}-${variant.buildType.name}-${variant.versionName}.apk"
|
||||||
output.outputFileName = outputFileName
|
output.outputFileName = outputFileName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,20 +71,20 @@ android {
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
signingConfig = signingConfigs.getByName("release")
|
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")
|
||||||
|
@ -98,7 +98,7 @@ android {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "17"
|
jvmTarget = Constants.JVM_TARGET
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
@ -115,14 +115,6 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("printVersionCode") {
|
|
||||||
doLast {
|
|
||||||
//print version code for CI
|
|
||||||
println(Constants.VERSION_CODE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val generalImplementation by configurations
|
val generalImplementation by configurations
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,5 +6,4 @@ 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>
|
||||||
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -110,6 +110,7 @@ class TunnelControlTile : TileService() {
|
||||||
|
|
||||||
private suspend fun updateTileState() {
|
private suspend fun updateTileState() {
|
||||||
vpnService.state.collect {
|
vpnService.state.collect {
|
||||||
|
try {
|
||||||
when(it) {
|
when(it) {
|
||||||
Tunnel.State.UP -> {
|
Tunnel.State.UP -> {
|
||||||
qsTile.state = Tile.STATE_ACTIVE
|
qsTile.state = Tile.STATE_ACTIVE
|
||||||
|
@ -124,6 +125,9 @@ class TunnelControlTile : TileService() {
|
||||||
val config = determineTileTunnel()
|
val config = determineTileTunnel()
|
||||||
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
|
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
|
||||||
qsTile.updateTile()
|
qsTile.updateTile()
|
||||||
|
} catch (e : Exception) {
|
||||||
|
Timber.e("Unable to update tile state")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
@ -231,14 +230,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
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,10 +40,11 @@ fun RowListItem(icon : @Composable() () -> Unit, text : String, onHold : () -> U
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(14.dp),
|
.padding(horizontal = 14.dp, vertical = 5.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
|
@ -38,8 +52,33 @@ fun RowListItem(icon : @Composable() () -> Unit, text : String, onHold : () -> U
|
||||||
icon()
|
icon()
|
||||||
Text(text)
|
Text(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
rowButton()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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) {
|
||||||
|
when(e) {
|
||||||
|
is WgTunnelException -> {
|
||||||
showSnackbarMessage(e.message)
|
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
|
||||||
|
.padding(bottom = 90.dp)
|
||||||
|
.onFocusChanged {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
fobColor = if (it.isFocused) hoverColor else secondaryColor }
|
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
,
|
,
|
||||||
onClick = {
|
onClick = {
|
||||||
|
@ -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) {
|
||||||
|
|
|
@ -276,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,
|
||||||
|
@ -290,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
|
||||||
) {
|
) {
|
||||||
|
@ -316,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)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSecondsBetweenTimestampAndNow(epoch : Long) : Long {
|
|
||||||
val time = Instant.ofEpochMilli(epoch)
|
val time = Instant.ofEpochMilli(epoch)
|
||||||
return Duration.between(time, Instant.now()).seconds
|
return Duration.between(time, Instant.now()).seconds
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
object Constants {
|
object Constants {
|
||||||
const val VERSION_NAME = "3.2.1"
|
const val VERSION_NAME = "3.2.2"
|
||||||
const val VERSION_CODE = 32100
|
const val JVM_TARGET = "17"
|
||||||
|
const val VERSION_CODE = 32200
|
||||||
const val TARGET_SDK = 34
|
const val TARGET_SDK = 34
|
||||||
const val MIN_SDK = 26
|
const val MIN_SDK = 26
|
||||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||||
|
const val APP_NAME = "wgtunnel"
|
||||||
|
|
||||||
const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD"
|
const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD"
|
||||||
const val KEY_ALIAS_VAR = "SIGNING_KEY_ALIAS"
|
const val KEY_ALIAS_VAR = "SIGNING_KEY_ALIAS"
|
||||||
const val KEY_PASS_VAR = "SIGNING_KEY_PASSWORD"
|
const val KEY_PASS_VAR = "SIGNING_KEY_PASSWORD"
|
||||||
const val KEY_STORE_PATH_VAR = "KEY_STORE_PATH"
|
const val KEY_STORE_PATH_VAR = "KEY_STORE_PATH"
|
||||||
|
|
||||||
|
const val RELEASE = "release"
|
||||||
|
const val TYPE = "type"
|
||||||
}
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
Enhancements:
|
||||||
|
- Add tunnel statistics to main screen
|
||||||
|
- Improve settings screen AndroidTV navigation
|
||||||
|
- Remove notification vibration
|
||||||
|
- Various other bug fixes
|
|
@ -1,6 +1,6 @@
|
||||||
[versions]
|
[versions]
|
||||||
accompanist = "0.32.0"
|
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"
|
||||||
|
@ -25,9 +25,9 @@ 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.5.0"
|
firebaseBom= "32.6.0"
|
||||||
compose="1.5.4"
|
compose="1.5.4"
|
||||||
crashlytics= "18.5.1"
|
crashlytics= "18.6.0"
|
||||||
analytics="21.5.0"
|
analytics="21.5.0"
|
||||||
composeCompiler="1.5.3"
|
composeCompiler="1.5.3"
|
||||||
zxingAndroidEmbedded = "4.3.0"
|
zxingAndroidEmbedded = "4.3.0"
|
||||||
|
|
Loading…
Reference in New Issue