diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index dd8ce7e..25d575d 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -70,21 +70,18 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: # 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 }} name: Release ${{ github.ref_name }} draft: false prerelease: false files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }} - deploy: - name: Deploy with fastlane - needs: build - runs-on: ubuntu-latest - steps: - - uses: ruby/setup-ruby@v1 + - 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) + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4af9338..e47f6d4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,7 +32,7 @@ android { } signingConfigs { - create("release") { + create(Constants.RELEASE) { val properties = Properties().apply { //created local file for signing details try { @@ -59,7 +59,7 @@ android { variant.outputs .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } .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 } } @@ -71,20 +71,20 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) - signingConfig = signingConfigs.getByName("release") + 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") @@ -98,7 +98,7 @@ android { isCoreLibraryDesugaringEnabled = true } kotlinOptions { - jvmTarget = "17" + jvmTarget = Constants.JVM_TARGET } buildFeatures { compose = true @@ -115,14 +115,6 @@ android { } } -tasks.register("printVersionCode") { - doLast { - //print version code for CI - println(Constants.VERSION_CODE) - } -} - - val generalImplementation by configurations dependencies { implementation(libs.androidx.core.ktx) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt index b8cdc1d..39cd44f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Constants.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Extensions.kt index 9e7efee..dfd0604 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/Extensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/Extensions.kt @@ -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) +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt index 6505c59..dcb204e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt @@ -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) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt index aad5568..6a1cd30 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt @@ -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 ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt index fb25515..e9fc3bd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt @@ -4,7 +4,6 @@ import android.net.NetworkCapabilities import kotlinx.coroutines.flow.Flow interface NetworkService { - fun getNetworkName(networkCapabilities: NetworkCapabilities) : String? - val networkStatus : Flow - -} \ No newline at end of file + fun getNetworkName(networkCapabilities: NetworkCapabilities): String? + val networkStatus: Flow +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt index fb3d476..29806ae 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/notification/NotificationService.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt index 37eee62..4f27cf9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt @@ -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() } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt index bbeaed9..d18ecf2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt @@ -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) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt index 7f58a99..186f7d3 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -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 @@ -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) - } } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt index 6a5387c..a78fedd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Routes.kt @@ -10,8 +10,7 @@ enum class Routes { Main, Settings, Support, - Config, - Detail; + Config; companion object { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt index c04b159..709cbda 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt @@ -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() } } } \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt deleted file mode 100644 index 07f90ad..0000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailScreen.kt +++ /dev/null @@ -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)) - }) - } - } - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt deleted file mode 100644 index 06498e8..0000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/detail/DetailViewModel.kt +++ /dev/null @@ -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(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)) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index 7ec21fa..de8f802 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -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() } } }) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index a22c18e..a1919be 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -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) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index 56b7665..6044634 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -276,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, @@ -290,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 ) { @@ -316,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() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt index 257a2d6..2aec795 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/NumberUtils.kt @@ -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 + } } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9572b63..50ca691 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -34,7 +34,7 @@ Tunnel on ethernet 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. Background Location Disclosure - 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! + 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! Enter SSID Submit SSID [Interface] @@ -130,7 +130,7 @@ Authentication failed Enable app shortcuts Export configs - Battery saver (experimental) + Battery saver (beta) Location services required Background location required Precise location required diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt index ac69a0d..03a81e3 100644 --- a/buildSrc/src/main/kotlin/Constants.kt +++ b/buildSrc/src/main/kotlin/Constants.kt @@ -1,12 +1,17 @@ object Constants { - const val VERSION_NAME = "3.2.1" - const val VERSION_CODE = 32100 + 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" } \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/32200.txt b/fastlane/metadata/android/en-US/changelogs/32200.txt new file mode 100644 index 0000000..ec75373 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/32200.txt @@ -0,0 +1,5 @@ +Enhancements: +- Add tunnel statistics to main screen +- Improve settings screen AndroidTV navigation +- Remove notification vibration +- Various other bug fixes \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fb5f4a6..630199c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] accompanist = "0.32.0" -activityCompose = "1.8.0" +activityCompose = "1.8.1" androidx-junit = "1.1.5" appcompat = "1.6.1" biometricKtx = "1.2.0-alpha05" @@ -25,9 +25,9 @@ androidGradlePlugin = "8.2.0-rc03" kotlin="1.9.10" ksp="1.9.10-1.0.13" composeBom="2023.10.01" -firebaseBom= "32.5.0" +firebaseBom= "32.6.0" compose="1.5.4" -crashlytics= "18.5.1" +crashlytics= "18.6.0" analytics="21.5.0" composeCompiler="1.5.3" zxingAndroidEmbedded = "4.3.0"