mirror of
https://github.com/wgtunnel/android.git
synced 2026-06-02 08:33:40 +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/release/output.json
|
||||
.idea/codeStyles/
|
||||
# where we keep our signing secrets locally
|
||||
app/signing.properties
|
||||
|
||||
+47
-12
@@ -1,3 +1,5 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
@@ -7,15 +9,15 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.zaneschepke.wireguardautotunnel"
|
||||
compileSdk = 34
|
||||
namespace = Constants.APP_ID
|
||||
compileSdk = Constants.TARGET_SDK
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.zaneschepke.wireguardautotunnel"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 32000
|
||||
versionName = "3.2.0"
|
||||
applicationId = Constants.APP_ID
|
||||
minSdk = Constants.MIN_SDK
|
||||
targetSdk = Constants.TARGET_SDK
|
||||
versionCode = Constants.VERSION_CODE
|
||||
versionName = Constants.VERSION_NAME
|
||||
|
||||
ksp {
|
||||
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 {
|
||||
//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 {
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
@@ -38,19 +71,20 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
signingConfig = signingConfigs.getByName(Constants.RELEASE)
|
||||
}
|
||||
debug {
|
||||
isDebuggable = true
|
||||
}
|
||||
}
|
||||
flavorDimensions.add("type")
|
||||
flavorDimensions.add(Constants.TYPE)
|
||||
productFlavors {
|
||||
create("fdroid") {
|
||||
dimension = "type"
|
||||
dimension = Constants.TYPE
|
||||
proguardFile("fdroid-rules.pro")
|
||||
}
|
||||
create("general") {
|
||||
dimension = "type"
|
||||
dimension = Constants.TYPE
|
||||
if (BuildHelper.isReleaseBuild(gradle) && BuildHelper.isGeneralFlavor(gradle))
|
||||
{
|
||||
apply(plugin = "com.google.gms.google-services")
|
||||
@@ -61,9 +95,10 @@ android {
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
jvmTarget = Constants.JVM_TARGET
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
@@ -105,6 +140,7 @@ dependencies {
|
||||
|
||||
//wg
|
||||
implementation(libs.tunnel)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
||||
//logging
|
||||
implementation(libs.timber)
|
||||
@@ -121,7 +157,6 @@ dependencies {
|
||||
implementation(libs.accompanist.systemuicontroller)
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.accompanist.flowlayout)
|
||||
implementation(libs.accompanist.navigation.animation)
|
||||
implementation(libs.accompanist.drawablepainter)
|
||||
|
||||
//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 WATCHER_SERVICE_WAKE_LOCK_TIMEOUT = 10*60*1000L /*10 minute*/
|
||||
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 FADE_IN_ANIMATION_DURATION = 1000
|
||||
const val SLIDE_IN_ANIMATION_DURATION = 500
|
||||
|
||||
@@ -5,6 +5,8 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
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(
|
||||
channelId = getString(R.string.watcher_channel_id),
|
||||
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)
|
||||
}
|
||||
|
||||
+3
@@ -120,6 +120,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
title = getString(R.string.tunnel_start_title),
|
||||
onGoing = false,
|
||||
vibration = false,
|
||||
showTimestamp = true,
|
||||
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
|
||||
)
|
||||
@@ -132,6 +133,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
channelName = getString(R.string.vpn_channel_name),
|
||||
title = getString(R.string.vpn_starting),
|
||||
onGoing = false,
|
||||
vibration = false,
|
||||
showTimestamp = true,
|
||||
description = getString(R.string.attempt_connection)
|
||||
)
|
||||
@@ -147,6 +149,7 @@ class WireGuardTunnelService : ForegroundService() {
|
||||
actionText = getString(R.string.restart),
|
||||
title = getString(R.string.vpn_connection_failed),
|
||||
onGoing = false,
|
||||
vibration = true,
|
||||
showTimestamp = true,
|
||||
description = message
|
||||
)
|
||||
|
||||
+3
-4
@@ -4,7 +4,6 @@ import android.net.NetworkCapabilities
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NetworkService<T> {
|
||||
fun getNetworkName(networkCapabilities: NetworkCapabilities) : String?
|
||||
val networkStatus : Flow<NetworkStatus>
|
||||
|
||||
}
|
||||
fun getNetworkName(networkCapabilities: NetworkCapabilities): String?
|
||||
val networkStatus: Flow<NetworkStatus>
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ interface NotificationService {
|
||||
description: String,
|
||||
showTimestamp : Boolean = false,
|
||||
importance: Int = NotificationManager.IMPORTANCE_HIGH,
|
||||
vibration: Boolean = true,
|
||||
vibration: Boolean = false,
|
||||
onGoing: Boolean = true,
|
||||
lights: Boolean = true
|
||||
): Notification
|
||||
|
||||
+16
-12
@@ -110,20 +110,24 @@ class TunnelControlTile : TileService() {
|
||||
|
||||
private suspend fun updateTileState() {
|
||||
vpnService.state.collect {
|
||||
when(it) {
|
||||
Tunnel.State.UP -> {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
}
|
||||
Tunnel.State.DOWN -> {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
}
|
||||
else -> {
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
try {
|
||||
when(it) {
|
||||
Tunnel.State.UP -> {
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
}
|
||||
Tunnel.State.DOWN -> {
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
}
|
||||
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
|
||||
|
||||
|
||||
class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
||||
) : VpnService {
|
||||
class WireGuardTunnel @Inject constructor(private val backend : Backend) : VpnService {
|
||||
|
||||
private val _tunnelName = MutableStateFlow("")
|
||||
override val tunnelName get() = _tunnelName.asStateFlow()
|
||||
@@ -115,11 +114,11 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
||||
_handshakeStatus.emit(HandshakeStatus.NOT_STARTED)
|
||||
}
|
||||
if(neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
|
||||
neverHadHandshakeCounter += 10
|
||||
neverHadHandshakeCounter += (1 * Constants.VPN_STATISTIC_CHECK_INTERVAL/1000).toInt()
|
||||
}
|
||||
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)
|
||||
} else {
|
||||
_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.unit.dp
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||
import com.google.accompanist.navigation.animation.composable
|
||||
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
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.prompt.CustomSnackBar
|
||||
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.settings.SettingsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||
@@ -67,7 +66,7 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
val navController = rememberAnimatedNavController()
|
||||
val navController = rememberNavController()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
WireguardAutoTunnelTheme {
|
||||
@@ -172,7 +171,7 @@ class MainActivity : AppCompatActivity() {
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
AnimatedNavHost(navController, startDestination = Routes.Main.name) {
|
||||
NavHost(navController, startDestination = Routes.Main.name) {
|
||||
composable(Routes.Main.name, enterTransition = {
|
||||
when (initialState.destination.route) {
|
||||
Routes.Settings.name, Routes.Support.name ->
|
||||
@@ -230,14 +229,6 @@ class MainActivity : AppCompatActivity() {
|
||||
val id = it.arguments?.getString("id")
|
||||
if(!id.isNullOrBlank()) {
|
||||
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,
|
||||
Settings,
|
||||
Support,
|
||||
Config,
|
||||
Detail;
|
||||
Config;
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import 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)
|
||||
@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(
|
||||
modifier = Modifier
|
||||
.animateContentSize()
|
||||
.clip(RoundedCornerShape(30.dp))
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
onClick()
|
||||
@@ -27,19 +40,45 @@ fun RowListItem(icon : @Composable() () -> Unit, text : String, onHold : () -> U
|
||||
}
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically,) {
|
||||
icon()
|
||||
Text(text)
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically,) {
|
||||
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 tunnelName by viewModel.tunnelName.collectAsStateWithLifecycle("")
|
||||
val settings by viewModel.settings.collectAsStateWithLifecycle()
|
||||
val statistics by viewModel.statistics.collectAsStateWithLifecycle(null)
|
||||
|
||||
// Nested scroll for control FAB
|
||||
val nestedScrollConnection = remember {
|
||||
@@ -171,8 +172,14 @@ fun MainScreen(
|
||||
scope.launch {
|
||||
try {
|
||||
viewModel.onTunnelQrResult(it.contents)
|
||||
} catch (e: WgTunnelException) {
|
||||
showSnackbarMessage(e.message)
|
||||
} catch (e: Exception) {
|
||||
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)
|
||||
var fobColor by remember { mutableStateOf(secondaryColor) }
|
||||
FloatingActionButton(
|
||||
modifier = Modifier.padding(bottom = 90.dp).onFocusChanged {
|
||||
if(WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor }
|
||||
modifier = Modifier
|
||||
.padding(bottom = 90.dp)
|
||||
.onFocusChanged {
|
||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
fobColor = if (it.isFocused) hoverColor else secondaryColor
|
||||
}
|
||||
}
|
||||
,
|
||||
onClick = {
|
||||
@@ -275,7 +285,7 @@ fun MainScreen(
|
||||
showBottomSheet = false
|
||||
try {
|
||||
tunnelFileImportResultLauncher.launch(Constants.ALLOWED_FILE_TYPES)
|
||||
} catch (e : Exception) {
|
||||
} catch (e: Exception) {
|
||||
showSnackbarMessage(e.message!!)
|
||||
}
|
||||
}
|
||||
@@ -362,17 +372,24 @@ fun MainScreen(
|
||||
HandshakeStatus.NEVER_CONNECTED -> brickRed
|
||||
} else {Color.Gray})
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val expanded = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
RowListItem(icon = {
|
||||
if (settings.isTunnelConfigDefault(tunnel))
|
||||
Icon(
|
||||
Icons.Rounded.Star, stringResource(R.string.status),
|
||||
tint = leadingIconColor,
|
||||
modifier = Modifier.padding(end = 10.dp).size(20.dp)
|
||||
modifier = Modifier
|
||||
.padding(end = 10.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
else Icon(
|
||||
Icons.Rounded.Circle, stringResource(R.string.status),
|
||||
tint = leadingIconColor,
|
||||
modifier = Modifier.padding(end = 15.dp).size(15.dp)
|
||||
modifier = Modifier
|
||||
.padding(end = 15.dp)
|
||||
.size(15.dp)
|
||||
)
|
||||
},
|
||||
text = tunnel.name,
|
||||
@@ -386,12 +403,16 @@ fun MainScreen(
|
||||
},
|
||||
onClick = {
|
||||
if (!WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
|
||||
if(state == Tunnel.State.UP && (tunnelName == tunnel.name) ) {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
} else {
|
||||
selectedTunnel = tunnel
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
},
|
||||
statistics = statistics,
|
||||
expanded = expanded.value,
|
||||
rowButton = {
|
||||
if (tunnel.id == selectedTunnel?.id && !WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||
Row {
|
||||
@@ -419,6 +440,15 @@ fun MainScreen(
|
||||
}
|
||||
}
|
||||
} 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)) {
|
||||
Row {
|
||||
if(!settings.isTunnelConfigDefault(tunnel)) {
|
||||
@@ -433,7 +463,9 @@ fun MainScreen(
|
||||
IconButton(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
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))
|
||||
}
|
||||
@@ -469,21 +501,10 @@ fun MainScreen(
|
||||
stringResource(id = R.string.delete)
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
|
||||
onCheckedChange = { checked ->
|
||||
onTunnelToggle(checked, tunnel)
|
||||
}
|
||||
)
|
||||
TunnelSwitch()
|
||||
}
|
||||
} else {
|
||||
Switch(
|
||||
checked = (state == Tunnel.State.UP && tunnel.name == tunnelName),
|
||||
onCheckedChange = { checked ->
|
||||
onTunnelToggle(checked, tunnel)
|
||||
}
|
||||
)
|
||||
TunnelSwitch()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -49,6 +49,7 @@ class MainViewModel @Inject constructor(
|
||||
val tunnelName get() = vpnService.tunnelName
|
||||
private val _settings = MutableStateFlow(Settings())
|
||||
val settings get() = _settings.asStateFlow()
|
||||
val statistics get() = vpnService.statistics
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
|
||||
+72
-56
@@ -99,7 +99,7 @@ fun SettingsScreen(
|
||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
var currentText by remember { mutableStateOf("") }
|
||||
val scrollState = rememberScrollState()
|
||||
var didShowLocationDisclaimer by remember { mutableStateOf(false) }
|
||||
var isLocationDisclaimerNeeded by remember { mutableStateOf(true) }
|
||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||
var showAuthPrompt by remember { mutableStateOf(false) }
|
||||
var didExportFiles by remember { mutableStateOf(false) }
|
||||
@@ -141,6 +141,7 @@ fun SettingsScreen(
|
||||
return(isBackgroundLocationGranted && fineLocationState.status.isGranted && !viewModel.isLocationServicesNeeded())
|
||||
}
|
||||
|
||||
|
||||
fun openSettings() {
|
||||
scope.launch {
|
||||
val intentSettings =
|
||||
@@ -156,62 +157,75 @@ fun SettingsScreen(
|
||||
rememberPermissionState(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
if(!backgroundLocationState.status.isGranted) {
|
||||
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 {
|
||||
isLocationDisclaimerNeeded = false
|
||||
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) {
|
||||
AuthorizationPrompt(onSuccess = {
|
||||
showAuthPrompt = false
|
||||
@@ -262,8 +276,8 @@ fun SettingsScreen(
|
||||
modifier = (if (WireGuardAutoTunnel.isRunningOnAndroidTv(context))
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth(fillMaxWidth)
|
||||
else Modifier.fillMaxWidth(fillMaxWidth)).padding(top = 60.dp, bottom = 25.dp)
|
||||
.fillMaxWidth(fillMaxWidth).padding(top = 10.dp)
|
||||
else Modifier.fillMaxWidth(fillMaxWidth).padding(top = 60.dp)).padding(bottom = 25.dp)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -276,8 +290,10 @@ fun SettingsScreen(
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(screenPadding, bottom = 5.dp, top = 5.dp)
|
||||
)
|
||||
val focus = Modifier.focusRequester(focusRequester)
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(screenPadding),
|
||||
modifier = (if(trustedSSIDs.isEmpty()) Modifier else
|
||||
focus).padding(screenPadding),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
@@ -302,7 +318,7 @@ fun SettingsScreen(
|
||||
value = currentText,
|
||||
onValueChange = { currentText = it },
|
||||
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)) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.math.pow
|
||||
|
||||
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()
|
||||
|
||||
fun bytesToKB(bytes : Long) : BigDecimal {
|
||||
return bytes.toBigDecimal().divide(BYTES_IN_KB.toBigDecimal())
|
||||
fun bytesToMB(bytes : Long) : BigDecimal {
|
||||
return bytes.toBigDecimal().divide(BYTES_IN_MB.toBigDecimal())
|
||||
}
|
||||
|
||||
fun isValidKey(key : String) : Boolean {
|
||||
@@ -22,13 +23,12 @@ object NumberUtils {
|
||||
return "tunnel${(Math.random() * 100000).toInt()}"
|
||||
}
|
||||
|
||||
fun formatDecimalTwoPlaces(bigDecimal: BigDecimal) : String {
|
||||
val df = DecimalFormat("#.##")
|
||||
return df.format(bigDecimal)
|
||||
}
|
||||
|
||||
fun getSecondsBetweenTimestampAndNow(epoch : Long) : Long {
|
||||
val time = Instant.ofEpochMilli(epoch)
|
||||
return Duration.between(time, Instant.now()).seconds
|
||||
fun getSecondsBetweenTimestampAndNow(epoch : Long) : Long? {
|
||||
return if (epoch != 0L) {
|
||||
val time = Instant.ofEpochMilli(epoch)
|
||||
return Duration.between(time, Instant.now()).seconds
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
<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_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_value_description">Submit SSID</string>
|
||||
<string name="config_validation">[Interface]</string>
|
||||
@@ -130,7 +130,7 @@
|
||||
<string name="authentication_failed">Authentication failed</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</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="background_location_required">Background location required</string>
|
||||
<string name="precise_location_required">Precise location required</string>
|
||||
|
||||
@@ -7,8 +7,6 @@ buildscript {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) 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]
|
||||
accompanist = "0.31.2-alpha"
|
||||
activityCompose = "1.8.0"
|
||||
accompanist = "0.32.0"
|
||||
activityCompose = "1.8.1"
|
||||
androidx-junit = "1.1.5"
|
||||
appcompat = "1.6.1"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
coreGoogleShortcuts = "1.1.0"
|
||||
coreKtx = "1.12.0"
|
||||
desugar_jdk_libs = "2.0.4"
|
||||
espressoCore = "3.5.1"
|
||||
firebase-crashlytics-gradle = "2.9.9"
|
||||
google-services = "4.4.0"
|
||||
hiltAndroid = "2.48"
|
||||
hiltNavigationCompose = "1.0.0"
|
||||
hiltAndroid = "2.48.1"
|
||||
hiltNavigationCompose = "1.1.0"
|
||||
junit = "4.13.2"
|
||||
kotlinx-serialization-json = "1.5.1"
|
||||
kotlinx-serialization-json = "1.6.0"
|
||||
lifecycle-runtime-compose = "2.6.2"
|
||||
material-icons-extended = "1.5.4"
|
||||
material3 = "1.1.2"
|
||||
navigationCompose = "2.7.4"
|
||||
navigationCompose = "2.7.5"
|
||||
roomVersion = "2.6.0"
|
||||
timber = "5.0.1"
|
||||
tunnel = "1.0.20230706"
|
||||
androidGradlePlugin = "8.3.0-alpha06"
|
||||
androidGradlePlugin = "8.2.0-rc03"
|
||||
kotlin="1.9.10"
|
||||
ksp="1.9.10-1.0.13"
|
||||
composeBom="2023.10.01"
|
||||
firebaseBom="32.4.0"
|
||||
firebaseBom= "32.6.0"
|
||||
compose="1.5.4"
|
||||
crashlytics="18.5.0"
|
||||
analytics="21.4.0"
|
||||
crashlytics= "18.6.0"
|
||||
analytics="21.5.0"
|
||||
composeCompiler="1.5.3"
|
||||
zxingAndroidEmbedded = "4.3.0"
|
||||
zxingCore = "3.4.1"
|
||||
zxingCore = "3.5.2"
|
||||
|
||||
|
||||
[libraries]
|
||||
# accompanist
|
||||
accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", 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-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" }
|
||||
|
||||
#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-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
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Reference in New Issue
Block a user