Compare commits

...

8 Commits

Author SHA1 Message Date
Zane Schepke 11ad494fbb 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
2023-11-23 21:04:26 -05:00
Zane Schepke 90b006acc5 change: build pipeline to fastlane deploy 2023-11-10 18:32:35 -05:00
Zane Schepke eb7b39c379 fix: build warnings and deprecations 2023-11-08 23:43:46 -05:00
Zane Schepke 0a17593310 add: properties to template 2023-11-08 22:26:36 -05:00
Zane Schepke c0e58125dd add: build and release CI
Add build and release pipeline for app

Fixes bug where location permission screen was not appearing on < Android 9

Bump versions
2023-11-08 22:11:52 -05:00
Zane Schepke 3791261f91 add: build and release CI
Add build and release pipeline for app

Fixes bug where location permission screen was not appearing on < Android 9

Bump versions
2023-11-08 21:58:17 -05:00
Zane Schepke d1e61be3ae Update issue templates 2023-11-04 11:45:00 -04:00
Zane Schepke afd4fb127f Update issue templates 2023-11-04 11:41:25 -04:00
33 changed files with 477 additions and 379 deletions
+31
View File
@@ -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.
+20
View File
@@ -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.
+87
View File
@@ -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)
+2
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"
+47 -12
View File
@@ -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
+4
View File
@@ -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)
}
@@ -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)
}
@@ -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
)
@@ -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>
}
@@ -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
@@ -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()
}
}
@@ -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()
}
}
}
@@ -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 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) {
@@ -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
}
}
}
+2 -2
View File
@@ -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>
-2
View File
@@ -7,8 +7,6 @@ buildscript {
}
}
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
+17
View File
@@ -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"
}
+2
View File
@@ -0,0 +1,2 @@
json_key_file "service_account.json"
package_name "com.zaneschepke.wireguardautotunnel"
+17
View File
@@ -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
View File
@@ -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
View File
@@ -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