mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e77966d70a | |||
| dcf213b63c | |||
| ca10586604 | |||
| 53480b0233 | |||
| 84de3a3991 | |||
| 820ff8a9ad | |||
| 1c0b54a8e4 | |||
| 75364f323c | |||
| b87aa75bf0 | |||
| c59e7d7637 | |||
| 28ef1a7683 | |||
| a5aadb42ed |
@@ -17,4 +17,18 @@ jobs:
|
||||
status: ${{ github.event.issue.state }} - #${{ github.event.issue.number }} ${{ github.event.issue.title }}
|
||||
https://github.com/zaneschepke/wgtunnel/issues/${{ github.event.issue.number }}'
|
||||
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
|
||||
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
|
||||
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ vars.TELEGRAM_ACTIVITY_TOPIC }}"
|
||||
|
||||
- name: Send Matrix Message
|
||||
run: |
|
||||
msg_text='${{ github.actor }} updated an issue:
|
||||
status: ${{ github.event.issue.state }} - #${{ github.event.issue.number }} ${{ github.event.issue.title }}
|
||||
https://github.com/zaneschepke/wgtunnel/issues/${{ github.event.issue.number }}'
|
||||
curl -s -X POST \
|
||||
-H "Authorization: Bearer ${{ secrets.MATRIX_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"msgtype": "m.text",
|
||||
"body": "'"$msg_text"'"
|
||||
}' \
|
||||
"https://matrix.yourserver.com/_matrix/client/v3/rooms/${{ vars.MATRIX_ACTIVITY_TOPIC }}/send/m.room.message/$(date +%s)"
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
name: on-publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
repository_dispatch:
|
||||
types: [ publish-release ]
|
||||
|
||||
jobs:
|
||||
|
||||
on-publish:
|
||||
name: On publish
|
||||
runs-on: ubuntu-latest
|
||||
@@ -18,4 +16,19 @@ jobs:
|
||||
${{ github.event.release.body }}
|
||||
https://github.com/zaneschepke/wgtunnel/releases/tag/${{ github.event.release.tag_name }}'
|
||||
curl -s -X POST 'https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendMessage' \
|
||||
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ secrets.TELEGRAM_TOPIC }}"
|
||||
-d "chat_id=${{ secrets.TELEGRAM_TO }}&text=${msg_text}&message_thread_id=${{ vars.TELEGRAM_RELEASE_TOPIC }}"
|
||||
|
||||
- name: Send Matrix Message
|
||||
run: |
|
||||
msg_text='${{ github.actor }} published a new release:
|
||||
Release: ${{ github.event.release.tag_name }}
|
||||
${{ github.event.release.body }}
|
||||
https://github.com/zaneschepke/wgtunnel/releases/tag/${{ github.event.release.tag_name }}'
|
||||
curl -s -X POST \
|
||||
-H "Authorization: Bearer ${{ secrets.MATRIX_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"msgtype": "m.text",
|
||||
"body": "'"$msg_text"'"
|
||||
}' \
|
||||
"https://matrix.yourserver.com/_matrix/client/v3/rooms/${{ vars.MATRIX_RELEASE_TOPIC }}/send/m.room.message/$(date +%s)"
|
||||
|
||||
@@ -193,6 +193,14 @@ jobs:
|
||||
files: |
|
||||
${{ github.workspace }}/temp/*
|
||||
|
||||
# notify socials
|
||||
- name: Trigger on-publish workflow
|
||||
if: ${{ inputs.release_type == 'release' }}
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
event-type: publish-release
|
||||
|
||||
publish-fdroid:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.permission.CONTROL_TUNNELS"
|
||||
android:label="@string/app_permission_title"
|
||||
android:description="@string/app_permission_description"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:protectionLevel="dangerous" />
|
||||
|
||||
@@ -45,6 +47,7 @@
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent>
|
||||
</queries>
|
||||
<application
|
||||
@@ -84,7 +87,7 @@
|
||||
<activity
|
||||
android:name=".core.shortcut.ShortcutsActivity"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:exported="false"
|
||||
android:noHistory="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:finishOnTaskLaunch="true"
|
||||
@@ -161,16 +164,17 @@
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".core.broadcast.RestartReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
android:exported="false"
|
||||
android:directBootAware="true">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
@@ -182,6 +186,20 @@
|
||||
<action android:name="com.wireguard.android.action.REFRESH_TUNNEL_STATES" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!--custom security solution for easier user integration-->
|
||||
<receiver
|
||||
android:name=".core.broadcast.RemoteControlReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true" tools:ignore="ExportedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="com.zaneschepke.wireguardautotunnel.START_TUNNEL" />
|
||||
<action android:name="com.zaneschepke.wireguardautotunnel.STOP_TUNNEL" />
|
||||
<action android:name="com.zaneschepke.wireguardautotunnel.START_AUTO_TUNNEL" />
|
||||
<action android:name="com.zaneschepke.wireguardautotunnel.STOP_AUTO_TUNNEL" />
|
||||
<action android:name="com.zaneschepke.wireguardautotunnel.START_KILL_SWITCH" />
|
||||
<action android:name="com.zaneschepke.wireguardautotunnel.STOP_KILL_SWITCH" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".core.broadcast.NotificationActionReceiver"
|
||||
android:exported="false"
|
||||
|
||||
@@ -81,7 +81,8 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.Appear
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.display.DisplayScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.language.LanguageScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.AutoTunnelScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AdvancedScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.AutoTunnelAdvancedScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced.SettingsAdvancedScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.disclosure.LocationDisclosureScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||
@@ -220,7 +221,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalNavController provides navController) {
|
||||
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
|
||||
WireguardAutoTunnelTheme(theme = appUiState.appState.theme) {
|
||||
VpnDeniedDialog(showVpnPermissionDialog, onDismiss = { showVpnPermissionDialog = false })
|
||||
|
||||
Scaffold(
|
||||
@@ -263,7 +264,7 @@ class MainActivity : AppCompatActivity() {
|
||||
route = Route.AutoTunnel,
|
||||
icon = Icons.Rounded.Bolt,
|
||||
onClick = {
|
||||
val route = if (appUiState.generalState.isLocationDisclosureShown) Route.AutoTunnel else Route.LocationDisclosure
|
||||
val route = if (appUiState.appState.isLocationDisclosureShown) Route.AutoTunnel else Route.LocationDisclosure
|
||||
navController.goFromRoot(route)
|
||||
},
|
||||
active = appUiState.isAutoTunnelActive,
|
||||
@@ -294,7 +295,7 @@ class MainActivity : AppCompatActivity() {
|
||||
) {
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination = (if (appUiState.generalState.isPinLockEnabled) Route.Lock else Route.Main),
|
||||
startDestination = (if (appUiState.appState.isPinLockEnabled) Route.Lock else Route.Main),
|
||||
) {
|
||||
composable<Route.Main> {
|
||||
MainScreen(appUiState, appViewState, viewModel)
|
||||
@@ -302,6 +303,9 @@ class MainActivity : AppCompatActivity() {
|
||||
composable<Route.Settings> {
|
||||
SettingsScreen(appUiState, appViewState, viewModel)
|
||||
}
|
||||
composable<Route.SettingsAdvanced> {
|
||||
SettingsAdvancedScreen(appUiState, viewModel)
|
||||
}
|
||||
composable<Route.LocationDisclosure> {
|
||||
LocationDisclosureScreen(appUiState, viewModel)
|
||||
}
|
||||
@@ -321,7 +325,7 @@ class MainActivity : AppCompatActivity() {
|
||||
SupportScreen()
|
||||
}
|
||||
composable<Route.AutoTunnelAdvanced> {
|
||||
AdvancedScreen(appUiState)
|
||||
AutoTunnelAdvancedScreen(appUiState, viewModel)
|
||||
}
|
||||
composable<Route.Logs> {
|
||||
LogsScreen(appViewState, viewModel)
|
||||
@@ -356,13 +360,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
BackHandler {
|
||||
if (navController.currentDestination?.route != Route.Main::class.qualifiedName) {
|
||||
navController.popBackStack()
|
||||
} else {
|
||||
this@MainActivity.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RemoteControlReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelManager: TunnelManager
|
||||
|
||||
enum class Action(private val suffix: String) {
|
||||
START_TUNNEL("START_TUNNEL"),
|
||||
STOP_TUNNEL("STOP_TUNNEL"),
|
||||
START_AUTO_TUNNEL("START_AUTO_TUNNEL"),
|
||||
STOP_AUTO_TUNNEL("STOP_AUTO_TUNNEL"),
|
||||
;
|
||||
|
||||
fun getFullAction(): String {
|
||||
return "${Constants.BASE_PACKAGE}.$suffix"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromAction(action: String): Action? {
|
||||
for (a in entries) {
|
||||
if (a.getFullAction() == action) {
|
||||
return a
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Timber.i("onReceive")
|
||||
val action = intent.action ?: return
|
||||
val appAction = Action.fromAction(action) ?: return Timber.w("Unknown action $action")
|
||||
applicationScope.launch {
|
||||
if (!appDataRepository.appState.isRemoteControlEnabled()) return@launch Timber.w("Remote control disabled")
|
||||
val key = appDataRepository.appState.getRemoteKey() ?: return@launch Timber.w("Remote control key missing")
|
||||
if (key != intent.getStringExtra(EXTRA_KEY)?.trim()) return@launch Timber.w("Invalid remote control key")
|
||||
when (appAction) {
|
||||
Action.START_TUNNEL -> {
|
||||
val tunnelName = intent.getStringExtra(EXTRA_TUN_NAME) ?: return@launch startDefaultTunnel()
|
||||
val tunnel = appDataRepository.tunnels.findByTunnelName(tunnelName) ?: return@launch startDefaultTunnel()
|
||||
tunnelManager.startTunnel(tunnel)
|
||||
}
|
||||
Action.STOP_TUNNEL -> {
|
||||
val tunnelName = intent.getStringExtra(EXTRA_TUN_NAME) ?: return@launch tunnelManager.stopTunnel()
|
||||
val tunnel = appDataRepository.tunnels.findByTunnelName(tunnelName) ?: return@launch tunnelManager.stopTunnel()
|
||||
tunnelManager.stopTunnel(tunnel)
|
||||
}
|
||||
Action.START_AUTO_TUNNEL -> serviceManager.startAutoTunnel()
|
||||
Action.STOP_AUTO_TUNNEL -> serviceManager.stopAutoTunnel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startDefaultTunnel() {
|
||||
appDataRepository.getPrimaryOrFirstTunnel()?.let { tunnel ->
|
||||
tunnelManager.startTunnel(tunnel)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_TUN_NAME = "tunnelName"
|
||||
const val EXTRA_KEY = "key"
|
||||
}
|
||||
}
|
||||
-8
@@ -35,14 +35,6 @@ class RestartReceiver : BroadcastReceiver() {
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action ?: return
|
||||
if (action != Intent.ACTION_BOOT_COMPLETED &&
|
||||
action != Intent.ACTION_MY_PACKAGE_REPLACED &&
|
||||
action != "com.htc.intent.action.QUICKBOOT_POWERON"
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
Timber.d("RestartReceiver triggered with action: ${intent.action}")
|
||||
serviceManager.updateTunnelTile()
|
||||
serviceManager.updateAutoTunnelTile()
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
@@ -11,9 +10,7 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -27,7 +24,6 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
abstract class BaseTunnel(
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
private val serviceManager: ServiceManager,
|
||||
@@ -60,6 +56,7 @@ abstract class BaseTunnel(
|
||||
val newState = state ?: existingState.status
|
||||
if (newState == TunnelStatus.Down) {
|
||||
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
|
||||
cleanUpTunThread(tunnelConf)
|
||||
current - originalConf
|
||||
} else if (existingState.status == newState && stats == null) {
|
||||
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState")
|
||||
@@ -79,19 +76,20 @@ abstract class BaseTunnel(
|
||||
activeTunnels.value.forEach { (config, state) ->
|
||||
if (state.status.isUpOrStarting()) {
|
||||
stopTunnel(config)
|
||||
delay(300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureTunnelCallbacks(tunnelConf: TunnelConf) {
|
||||
Timber.d("Configuring TunnelConf instance: ${tunnelConf.hashCode()}")
|
||||
|
||||
tunnelConf.setStateChangeCallback { state ->
|
||||
Timber.d("State change callback triggered for tunnel ${tunnelConf.id}: ${tunnelConf.tunName} with state $state at ${System.currentTimeMillis()}")
|
||||
when (state) {
|
||||
is Tunnel.State -> applicationScope.launch { updateTunnelStatus(tunnelConf, state.asTunnelState()) }
|
||||
is org.amnezia.awg.backend.Tunnel.State -> applicationScope.launch { updateTunnelStatus(tunnelConf, state.asTunnelState()) }
|
||||
applicationScope.launch {
|
||||
Timber.d("State change callback triggered for tunnel ${tunnelConf.id}: ${tunnelConf.tunName} with state $state at ${System.currentTimeMillis()}")
|
||||
when (state) {
|
||||
is Tunnel.State -> updateTunnelStatus(tunnelConf, state.asTunnelState())
|
||||
is org.amnezia.awg.backend.Tunnel.State -> updateTunnelStatus(tunnelConf, state.asTunnelState())
|
||||
}
|
||||
handleServiceChangesOnStop()
|
||||
}
|
||||
serviceManager.updateTunnelTile()
|
||||
}
|
||||
@@ -144,9 +142,7 @@ abstract class BaseTunnel(
|
||||
if (tunnelConf == null) return stopActiveTunnels()
|
||||
tunMutex.withLock {
|
||||
try {
|
||||
val stuckStarting = activeTuns.isStarting(tunnelConf.id)
|
||||
handleTunnelThreadCleanup(tunnelConf)
|
||||
if (stuckStarting) return Timber.d("Stuck in starting, so just shutting down tunnel thread")
|
||||
if (activeTuns.isStarting(tunnelConf.id)) return handleStuckStartingTunnelShutdown(tunnelConf)
|
||||
updateTunnelStatus(tunnelConf, TunnelStatus.Stopping(reason))
|
||||
stopTunnelInner(tunnelConf)
|
||||
} catch (e: BackendError) {
|
||||
@@ -161,15 +157,14 @@ abstract class BaseTunnel(
|
||||
stopBackend(tunnel)
|
||||
saveTunnelActiveState(tunnelConf, false)
|
||||
removeActiveTunnel(tunnel)
|
||||
handleServiceChangesOnStop()
|
||||
}
|
||||
|
||||
private suspend fun handleServiceChangesOnStop() {
|
||||
if (activeTuns.value.isEmpty() && !isBouncing.get()) return serviceManager.stopTunnelForegroundService()
|
||||
}
|
||||
|
||||
private suspend fun handleTunnelThreadCleanup(tunnel: TunnelConf) {
|
||||
Timber.d("Cleaning up thread for ${tunnel.name}")
|
||||
private suspend fun handleStuckStartingTunnelShutdown(tunnel: TunnelConf) {
|
||||
Timber.d("Stuck in starting state so shutting down tunnel thread for tunnel ${tunnel.name}")
|
||||
try {
|
||||
tunThreads[tunnel.id]?.let {
|
||||
if (it.state != Thread.State.TERMINATED) {
|
||||
@@ -182,6 +177,10 @@ abstract class BaseTunnel(
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}")
|
||||
}
|
||||
cleanUpTunThread(tunnel)
|
||||
}
|
||||
|
||||
private fun cleanUpTunThread(tunnel: TunnelConf) {
|
||||
Timber.d("Removing thread for ${tunnel.name}")
|
||||
tunThreads -= tunnel.id
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
@@ -13,18 +12,16 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class KernelTunnel @Inject constructor(
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
serviceManager: ServiceManager,
|
||||
appDataRepository: AppDataRepository,
|
||||
private val backend: Backend,
|
||||
) : BaseTunnel(ioDispatcher, applicationScope, appDataRepository, serviceManager) {
|
||||
) : BaseTunnel(applicationScope, appDataRepository, serviceManager) {
|
||||
|
||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
||||
return try {
|
||||
|
||||
+1
-4
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
|
||||
@@ -12,7 +11,6 @@ import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.amnezia.awg.backend.Backend
|
||||
import org.amnezia.awg.backend.BackendException
|
||||
@@ -23,12 +21,11 @@ import javax.inject.Inject
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
class UserspaceTunnel @Inject constructor(
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
val serviceManager: ServiceManager,
|
||||
val appDataRepository: AppDataRepository,
|
||||
private val backend: Backend,
|
||||
) : BaseTunnel(ioDispatcher, applicationScope, appDataRepository, serviceManager) {
|
||||
) : BaseTunnel(applicationScope, appDataRepository, serviceManager) {
|
||||
|
||||
private var previousBackendState: Pair<BackendState, Boolean>? = null
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ class DataStoreManager(
|
||||
val isLocalLogsEnabled = booleanPreferencesKey("LOCAL_LOGS_ENABLED")
|
||||
val locale = stringPreferencesKey("LOCALE")
|
||||
val theme = stringPreferencesKey("THEME")
|
||||
val isRemoteControlEnabled = booleanPreferencesKey("IS_REMOTE_CONTROL_ENABLED")
|
||||
val remoteKey = stringPreferencesKey("REMOTE_KEY")
|
||||
}
|
||||
|
||||
// preferences
|
||||
|
||||
@@ -9,6 +9,8 @@ data class GeneralState(
|
||||
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
|
||||
val isTunnelStatsExpanded: Boolean = IS_TUNNEL_STATS_EXPANDED,
|
||||
val isLocalLogsEnabled: Boolean = IS_LOGS_ENABLED_DEFAULT,
|
||||
val isRemoteControlEnabled: Boolean = IS_REMOTE_CONTROL_ENABLED,
|
||||
val remoteKey: String? = null,
|
||||
val locale: String? = null,
|
||||
val theme: Theme = Theme.AUTOMATIC,
|
||||
) {
|
||||
@@ -19,6 +21,8 @@ data class GeneralState(
|
||||
isPinLockEnabled,
|
||||
isTunnelStatsExpanded,
|
||||
isLocationDisclosureShown,
|
||||
isRemoteControlEnabled,
|
||||
remoteKey,
|
||||
locale,
|
||||
theme,
|
||||
)
|
||||
@@ -31,7 +35,9 @@ data class GeneralState(
|
||||
isBatteryOptimizationDisableShown,
|
||||
isPinLockEnabled,
|
||||
isTunnelStatsExpanded,
|
||||
isLocationDisclosureShown,
|
||||
isLocalLogsEnabled,
|
||||
isRemoteControlEnabled,
|
||||
remoteKey,
|
||||
locale,
|
||||
theme,
|
||||
)
|
||||
@@ -42,5 +48,6 @@ data class GeneralState(
|
||||
const val PIN_LOCK_ENABLED_DEFAULT = false
|
||||
const val IS_TUNNEL_STATS_EXPANDED = false
|
||||
const val IS_LOGS_ENABLED_DEFAULT = false
|
||||
const val IS_REMOTE_CONTROL_ENABLED = false
|
||||
}
|
||||
}
|
||||
|
||||
+21
-2
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.data.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.DataStoreManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -78,7 +79,23 @@ class DataStoreAppStateRepository(
|
||||
return dataStoreManager.getFromStore(DataStoreManager.locale)
|
||||
}
|
||||
|
||||
override val flow: Flow<GeneralState> =
|
||||
override suspend fun setIsRemoteControlEnabled(enabled: Boolean) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.isRemoteControlEnabled, enabled)
|
||||
}
|
||||
|
||||
override suspend fun isRemoteControlEnabled(): Boolean {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.isRemoteControlEnabled) ?: GeneralState.IS_REMOTE_CONTROL_ENABLED
|
||||
}
|
||||
|
||||
override suspend fun setRemoteKey(key: String) {
|
||||
dataStoreManager.saveToDataStore(DataStoreManager.remoteKey, key)
|
||||
}
|
||||
|
||||
override suspend fun getRemoteKey(): String? {
|
||||
return dataStoreManager.getFromStore(DataStoreManager.remoteKey)
|
||||
}
|
||||
|
||||
override val flow: Flow<AppState> =
|
||||
dataStoreManager.preferencesFlow.map { prefs ->
|
||||
prefs?.let { pref ->
|
||||
try {
|
||||
@@ -94,6 +111,8 @@ class DataStoreAppStateRepository(
|
||||
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT,
|
||||
isTunnelStatsExpanded = pref[DataStoreManager.tunnelStatsExpanded] ?: GeneralState.IS_TUNNEL_STATS_EXPANDED,
|
||||
isLocalLogsEnabled = pref[DataStoreManager.isLocalLogsEnabled] ?: GeneralState.IS_LOGS_ENABLED_DEFAULT,
|
||||
isRemoteControlEnabled = pref[DataStoreManager.isRemoteControlEnabled] ?: GeneralState.IS_REMOTE_CONTROL_ENABLED,
|
||||
remoteKey = pref[DataStoreManager.remoteKey],
|
||||
locale = pref[DataStoreManager.locale],
|
||||
theme = getTheme(),
|
||||
)
|
||||
@@ -102,5 +121,5 @@ class DataStoreAppStateRepository(
|
||||
GeneralState()
|
||||
}
|
||||
} ?: GeneralState()
|
||||
}
|
||||
}.map { it.toAppState() }
|
||||
}
|
||||
|
||||
+2
-2
@@ -30,9 +30,9 @@ class RoomTunnelRepository(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveAll(tunnelConfs: List<TunnelConf>) {
|
||||
override suspend fun saveAll(tunnelConfList: List<TunnelConf>) {
|
||||
withContext(ioDispatcher) {
|
||||
tunnelConfigDao.saveAll(tunnelConfs.map(TunnelConfig::from))
|
||||
tunnelConfigDao.saveAll(tunnelConfList.map(TunnelConfig::from))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,26 +62,24 @@ class TunnelModule {
|
||||
@Singleton
|
||||
@Kernel
|
||||
fun provideKernelProvider(
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
serviceManager: ServiceManager,
|
||||
appDataRepository: AppDataRepository,
|
||||
backend: com.wireguard.android.backend.Backend,
|
||||
): TunnelProvider {
|
||||
return KernelTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, backend)
|
||||
return KernelTunnel(applicationScope, serviceManager, appDataRepository, backend)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Userspace
|
||||
fun provideUserspaceProvider(
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
serviceManager: ServiceManager,
|
||||
appDataRepository: AppDataRepository,
|
||||
backend: Backend,
|
||||
): TunnelProvider {
|
||||
return UserspaceTunnel(ioDispatcher, applicationScope, serviceManager, appDataRepository, backend)
|
||||
return UserspaceTunnel(applicationScope, serviceManager, appDataRepository, backend)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -8,6 +8,8 @@ data class AppState(
|
||||
val isPinLockEnabled: Boolean,
|
||||
val isTunnelStatsExpanded: Boolean,
|
||||
val isLocalLogsEnabled: Boolean,
|
||||
val isRemoteControlEnabled: Boolean,
|
||||
val remoteKey: String?,
|
||||
val locale: String?,
|
||||
val theme: Theme,
|
||||
)
|
||||
|
||||
+10
-2
@@ -1,6 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.repository
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -33,5 +33,13 @@ interface AppStateRepository {
|
||||
|
||||
suspend fun getLocale(): String?
|
||||
|
||||
val flow: Flow<GeneralState>
|
||||
suspend fun setIsRemoteControlEnabled(enabled: Boolean)
|
||||
|
||||
suspend fun isRemoteControlEnabled(): Boolean
|
||||
|
||||
suspend fun setRemoteKey(key: String)
|
||||
|
||||
suspend fun getRemoteKey(): String?
|
||||
|
||||
val flow: Flow<AppState>
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ sealed class Route {
|
||||
@Serializable
|
||||
data object Settings : Route()
|
||||
|
||||
@Serializable
|
||||
data object SettingsAdvanced : Route()
|
||||
|
||||
@Serializable
|
||||
data object AutoTunnel : Route()
|
||||
|
||||
|
||||
+1
-2
@@ -41,7 +41,6 @@ fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp)
|
||||
.weight(4f, false)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
@@ -71,7 +70,7 @@ fun SurfaceSelectionGroupButton(items: List<SelectionItem>) {
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
modifier = Modifier
|
||||
.padding(end = 24.dp, start = 16.dp)
|
||||
.padding(start = 16.dp)
|
||||
.weight(1f),
|
||||
) {
|
||||
it()
|
||||
|
||||
+18
@@ -4,6 +4,7 @@ import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -20,15 +21,21 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
|
||||
@Composable
|
||||
fun BottomBarTabs(tabs: List<BottomNavItem>, selectedTabIndex: Int, isChildRoute: Boolean, onTabSelected: (BottomNavItem) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val isRunningOnTv = remember { context.isRunningOnTv() }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -44,6 +51,17 @@ fun BottomBarTabs(tabs: List<BottomNavItem>, selectedTabIndex: Int, isChildRoute
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.background(Color.Transparent)
|
||||
.then(
|
||||
if (isRunningOnTv) {
|
||||
Modifier.clickable {
|
||||
if (index == selectedTabIndex && !isChildRoute) return@clickable
|
||||
tab.onClick.invoke()
|
||||
onTabSelected(tab)
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures {
|
||||
if (index == selectedTabIndex && !isChildRoute) return@detectTapGestures
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ fun currentNavBackStackEntryAsNavBarState(
|
||||
route = Route.AutoTunnel,
|
||||
)
|
||||
}
|
||||
backStackEntry.isCurrentRoute(Route.AutoTunnelAdvanced::class) -> {
|
||||
backStackEntry.isCurrentRoute(Route.AutoTunnelAdvanced::class) || backStackEntry.isCurrentRoute(Route.SettingsAdvanced::class) -> {
|
||||
NavBarState(
|
||||
showTop = true, showBottom = true,
|
||||
{ Text(stringResource(R.string.advanced_settings)) },
|
||||
|
||||
+4
-1
@@ -21,7 +21,9 @@ import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.NetworkTunnelingItems
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.WifiTunnelingItems
|
||||
@@ -37,6 +39,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
@Composable
|
||||
fun AutoTunnelScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val fineLocationState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
var currentText by remember { mutableStateOf("") }
|
||||
var isBackgroundLocationGranted by remember { mutableStateOf(true) }
|
||||
@@ -103,7 +106,7 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||
items = NetworkTunnelingItems(uiState, viewModel),
|
||||
)
|
||||
SurfaceSelectionGroupButton(
|
||||
items = listOf(AdvancedSettingsItem()),
|
||||
items = listOf(AdvancedSettingsItem(onClick = { navController.navigate(Route.AutoTunnelAdvanced) })),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-5
@@ -10,16 +10,13 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.advanced.components.DebounceDelaySelector
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
|
||||
@Composable
|
||||
fun AdvancedScreen(appUiState: AppUiState) {
|
||||
val appViewModel: AppViewModel = hiltViewModel()
|
||||
|
||||
fun AutoTunnelAdvancedScreen(appUiState: AppUiState, viewModel: AppViewModel) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
|
||||
@@ -31,7 +28,7 @@ fun AdvancedScreen(appUiState: AppUiState) {
|
||||
) {
|
||||
DebounceDelaySelector(
|
||||
currentDelay = appUiState.appSettings.debounceDelaySeconds,
|
||||
onEvent = appViewModel::handleEvent,
|
||||
onEvent = viewModel::handleEvent,
|
||||
)
|
||||
}
|
||||
}
|
||||
+3
-6
@@ -7,14 +7,11 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||
|
||||
@Composable
|
||||
fun AdvancedSettingsItem(): SelectionItem {
|
||||
val navController = LocalNavController.current
|
||||
fun AdvancedSettingsItem(onClick: () -> Unit): SelectionItem {
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Outlined.Settings,
|
||||
title = {
|
||||
@@ -24,8 +21,8 @@ fun AdvancedSettingsItem(): SelectionItem {
|
||||
)
|
||||
},
|
||||
trailing = {
|
||||
ForwardButton { navController.navigate(Route.AutoTunnelAdvanced) }
|
||||
ForwardButton { onClick() }
|
||||
},
|
||||
onClick = { navController.navigate(Route.AutoTunnelAdvanced) },
|
||||
onClick = { onClick() },
|
||||
)
|
||||
}
|
||||
|
||||
+7
@@ -1,5 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -21,8 +22,10 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
@@ -46,6 +49,7 @@ fun WifiTunnelingItems(
|
||||
isWifiNameReadable: () -> Boolean,
|
||||
): List<SelectionItem> {
|
||||
val context = LocalContext.current
|
||||
val clipboard = LocalClipboardManager.current
|
||||
|
||||
val baseItems = listOf(
|
||||
SelectionItem(
|
||||
@@ -76,6 +80,9 @@ fun WifiTunnelingItems(
|
||||
style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.clickable {
|
||||
wifiName?.let { clipboard.setText(AnnotatedString(it)) }
|
||||
},
|
||||
)
|
||||
},
|
||||
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnWifi) },
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ fun TunnelList(
|
||||
val tunnelState = activeTunnels.getValueById(tunnel.id) ?: TunnelState()
|
||||
TunnelRowItem(
|
||||
isActive = tunnelState.status.isUpOrStarting(),
|
||||
expanded = appUiState.generalState.isTunnelStatsExpanded,
|
||||
expanded = appUiState.appState.isTunnelStatsExpanded,
|
||||
isSelected = selectedTunnel?.id == tunnel.id,
|
||||
tunnel = tunnel,
|
||||
tunnelState = tunnelState,
|
||||
|
||||
+8
-1
@@ -15,7 +15,10 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components.AdvancedSettingsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AlwaysOnVpnItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AppShortcutsItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.AppearanceItem
|
||||
@@ -36,6 +39,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
fun SettingsScreen(uiState: AppUiState, appViewState: AppViewState, viewModel: AppViewModel) {
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val navController = LocalNavController.current
|
||||
val isRunningOnTv = remember { context.isRunningOnTv() }
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
@@ -76,7 +80,7 @@ fun SettingsScreen(uiState: AppUiState, appViewState: AppViewState, viewModel: A
|
||||
items = buildList {
|
||||
add(AppearanceItem())
|
||||
add(LocalLoggingItem(uiState, viewModel))
|
||||
if (uiState.generalState.isLocalLogsEnabled) add(ReadLogsItem())
|
||||
if (uiState.appState.isLocalLogsEnabled) add(ReadLogsItem())
|
||||
add(PinLockItem(uiState, viewModel))
|
||||
},
|
||||
)
|
||||
@@ -85,5 +89,8 @@ fun SettingsScreen(uiState: AppUiState, appViewState: AppViewState, viewModel: A
|
||||
items = listOf(KernelModeItem(uiState, viewModel)),
|
||||
)
|
||||
}
|
||||
SurfaceSelectionGroupButton(
|
||||
items = listOf(AdvancedSettingsItem(onClick = { navController.navigate(Route.SettingsAdvanced) })),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced
|
||||
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.MainActivity
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced.components.RemoteControlItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
|
||||
@Composable
|
||||
fun SettingsAdvancedScreen(appUiState: AppUiState, viewModel: AppViewModel) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val activity = context as? MainActivity
|
||||
|
||||
// Secure screen due to sensitive information
|
||||
DisposableEffect(Unit) {
|
||||
activity?.window?.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
)
|
||||
onDispose {
|
||||
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(top = 24.dp)
|
||||
.padding(horizontal = 24.dp),
|
||||
) {
|
||||
SurfaceSelectionGroupButton(
|
||||
listOf(
|
||||
RemoteControlItem(appUiState, viewModel),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.advanced.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.SmartToy
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
|
||||
|
||||
@Composable
|
||||
fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
return SelectionItem(
|
||||
leadingIcon = Icons.Filled.SmartToy,
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = uiState.appState.isRemoteControlEnabled,
|
||||
onClick = { viewModel.handleEvent(AppEvent.ToggleRemoteControl) },
|
||||
)
|
||||
},
|
||||
description = {
|
||||
uiState.appState.remoteKey?.let { key ->
|
||||
AnimatedVisibility(visible = uiState.appState.isRemoteControlEnabled) {
|
||||
Text(
|
||||
text = stringResource(R.string.remote_key_template, key),
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.clickable {
|
||||
clipboardManager.setText(AnnotatedString(key))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.enable_remote_app_control),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
},
|
||||
onClick = { viewModel.handleEvent(AppEvent.ToggleRemoteControl) },
|
||||
)
|
||||
}
|
||||
+4
-4
@@ -33,22 +33,22 @@ fun DisplayScreen(appUiState: AppUiState, viewModel: AppViewModel) {
|
||||
onClick = {
|
||||
viewModel.handleEvent(AppEvent.SetTheme(Theme.AUTOMATIC))
|
||||
},
|
||||
selected = appUiState.generalState.theme == Theme.AUTOMATIC,
|
||||
selected = appUiState.appState.theme == Theme.AUTOMATIC,
|
||||
)
|
||||
IconSurfaceButton(
|
||||
title = stringResource(R.string.light),
|
||||
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.LIGHT)) },
|
||||
selected = appUiState.generalState.theme == Theme.LIGHT,
|
||||
selected = appUiState.appState.theme == Theme.LIGHT,
|
||||
)
|
||||
IconSurfaceButton(
|
||||
title = stringResource(R.string.dark),
|
||||
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.DARK)) },
|
||||
selected = appUiState.generalState.theme == Theme.DARK,
|
||||
selected = appUiState.appState.theme == Theme.DARK,
|
||||
)
|
||||
IconSurfaceButton(
|
||||
title = stringResource(R.string.dynamic),
|
||||
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.DYNAMIC)) },
|
||||
selected = appUiState.generalState.theme == Theme.DYNAMIC,
|
||||
selected = appUiState.appState.theme == Theme.DYNAMIC,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ fun AutomaticLanguageItem(appUiState: AppUiState, viewModel: AppViewModel) {
|
||||
viewModel.handleEvent(AppEvent.SetLocale(LocaleUtil.OPTION_PHONE_LANGUAGE))
|
||||
},
|
||||
trailing = {
|
||||
with(appUiState.generalState.locale) {
|
||||
with(appUiState.appState.locale) {
|
||||
if (this == LocaleUtil.OPTION_PHONE_LANGUAGE || this == null) {
|
||||
SelectedLabel()
|
||||
}
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ fun LanguageItem(locale: Locale, appUiState: AppUiState, viewModel: AppViewModel
|
||||
viewModel.handleEvent(AppEvent.SetLocale(locale.toLanguageTag()))
|
||||
},
|
||||
trailing = {
|
||||
if (locale.toLanguageTag() == appUiState.generalState.locale) {
|
||||
if (locale.toLanguageTag() == appUiState.appState.locale) {
|
||||
SelectedLabel()
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ fun LocalLoggingItem(uiState: AppUiState, viewModel: AppViewModel): SelectionIte
|
||||
description = { SelectionItemLabel(R.string.enable_local_logging, isDescription = true) },
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = uiState.generalState.isLocalLogsEnabled,
|
||||
checked = uiState.appState.isLocalLogsEnabled,
|
||||
onClick = { viewModel.handleEvent(AppEvent.ToggleLocalLogging) },
|
||||
)
|
||||
},
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ fun PinLockItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
val context = LocalContext.current
|
||||
|
||||
fun onPinLockToggle() {
|
||||
if (uiState.generalState.isPinLockEnabled) {
|
||||
if (uiState.appState.isPinLockEnabled) {
|
||||
viewModel.handleEvent(AppEvent.TogglePinLock)
|
||||
} else {
|
||||
PinManager.initialize(context)
|
||||
@@ -41,7 +41,7 @@ fun PinLockItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
|
||||
},
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = uiState.generalState.isPinLockEnabled,
|
||||
checked = uiState.appState.isPinLockEnabled,
|
||||
onClick = { onPinLockToggle() },
|
||||
)
|
||||
},
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ fun LocationDisclosureScreen(appUiState: AppUiState, viewModel: AppViewModel) {
|
||||
val navController = LocalNavController.current
|
||||
|
||||
LaunchedEffect(Unit, appUiState) {
|
||||
if (appUiState.generalState.isLocationDisclosureShown) navController.goFromRoot(Route.AutoTunnel)
|
||||
if (appUiState.appState.isLocationDisclosureShown) navController.goFromRoot(Route.AutoTunnel)
|
||||
}
|
||||
|
||||
Column(
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.ui.state
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
|
||||
@@ -10,7 +11,7 @@ data class AppUiState(
|
||||
val appSettings: AppSettings = AppSettings(),
|
||||
val tunnels: List<TunnelConf> = emptyList(),
|
||||
val activeTunnels: Map<TunnelConf, TunnelState> = emptyMap(),
|
||||
val generalState: GeneralState = GeneralState(),
|
||||
val appState: AppState = GeneralState().toAppState(),
|
||||
val isAutoTunnelActive: Boolean = false,
|
||||
val appConfigurationChange: Boolean = false,
|
||||
val isAppLoaded: Boolean = false,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util
|
||||
|
||||
object Constants {
|
||||
|
||||
const val BASE_PACKAGE = "com.zaneschepke.wireguardautotunnel"
|
||||
|
||||
const val BASE_LOG_FILE_NAME = "wg_tunnel_logs"
|
||||
const val LOG_BUFFER_SIZE = 10_000L
|
||||
|
||||
const val MANUAL_TUNNEL_CONFIG_ID = 0
|
||||
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
|
||||
|
||||
@@ -14,11 +14,11 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||
import com.zaneschepke.wireguardautotunnel.di.AppShell
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.ConfigType
|
||||
@@ -56,6 +56,7 @@ import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
import java.net.URL
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
@@ -99,7 +100,7 @@ constructor(
|
||||
) { array ->
|
||||
val settings = array[0] as AppSettings
|
||||
val tunnels = array[1] as List<TunnelConf>
|
||||
val generalState = array[2] as GeneralState
|
||||
val generalState = array[2] as AppState
|
||||
val activeTunnels = array[3] as Map<TunnelConf, TunnelState>
|
||||
val autoTunnel = array[4] as Boolean
|
||||
val network = array[5] as NetworkStatus
|
||||
@@ -108,7 +109,7 @@ constructor(
|
||||
appSettings = settings,
|
||||
tunnels = tunnels,
|
||||
activeTunnels = activeTunnels,
|
||||
generalState = generalState,
|
||||
appState = generalState,
|
||||
isAutoTunnelActive = autoTunnel,
|
||||
isAppLoaded = true,
|
||||
networkStatus = network,
|
||||
@@ -122,10 +123,10 @@ constructor(
|
||||
init {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
uiState.withFirstState { state ->
|
||||
initPin(state.generalState.isPinLockEnabled)
|
||||
initPin(state.appState.isPinLockEnabled)
|
||||
handleKillSwitchChange(state.appSettings)
|
||||
initServicesFromSavedState(state)
|
||||
if (state.generalState.isLocalLogsEnabled) startCollectingLogs()
|
||||
if (state.appState.isLocalLogsEnabled) startCollectingLogs()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,7 +134,7 @@ constructor(
|
||||
fun handleEvent(event: AppEvent) = viewModelScope.launch(ioDispatcher) {
|
||||
uiState.withFirstState { state ->
|
||||
when (event) {
|
||||
AppEvent.ToggleLocalLogging -> handleToggleLocalLogging(state.generalState.isLocalLogsEnabled)
|
||||
AppEvent.ToggleLocalLogging -> handleToggleLocalLogging(state.appState.isLocalLogsEnabled)
|
||||
is AppEvent.SetDebounceDelay -> handleSetDebounceDelay(state.appSettings, event.delay)
|
||||
is AppEvent.CopyTunnel -> handleCopyTunnel(event.tunnel, state.tunnels)
|
||||
is AppEvent.DeleteTunnel -> handleDeleteTunnel(event.tunnel, state)
|
||||
@@ -145,9 +146,9 @@ constructor(
|
||||
is AppEvent.StartTunnel -> handleStartTunnel(event.tunnel, state.appSettings)
|
||||
is AppEvent.StopTunnel -> handleStopTunnel(event.tunnel)
|
||||
AppEvent.ToggleAutoTunnel -> handleToggleAutoTunnel(state)
|
||||
AppEvent.ToggleTunnelStatsExpanded -> handleToggleExpandTunnelStats(state.generalState.isTunnelStatsExpanded)
|
||||
AppEvent.ToggleTunnelStatsExpanded -> handleToggleExpandTunnelStats(state.appState.isTunnelStatsExpanded)
|
||||
AppEvent.ToggleAlwaysOn -> handleToggleAlwaysOnVPN(state.appSettings)
|
||||
AppEvent.TogglePinLock -> handlePinLockToggled(state.generalState.isPinLockEnabled)
|
||||
AppEvent.TogglePinLock -> handlePinLockToggled(state.appState.isPinLockEnabled)
|
||||
AppEvent.SetLocationDisclosureShown -> setLocationDisclosureShown()
|
||||
is AppEvent.SetLocale -> handleLocaleChange(event.localeTag)
|
||||
AppEvent.ToggleRestartAtBoot -> handleToggleRestartAtBoot(state.appSettings)
|
||||
@@ -188,10 +189,17 @@ constructor(
|
||||
is AppEvent.ShowMessage -> handleShowMessage(event.message)
|
||||
is AppEvent.PopBackStack -> _appViewState.update { it.copy(popBackStack = event.pop) }
|
||||
is AppEvent.ClearTunnelError -> tunnelManager.clearError(event.tunnel)
|
||||
AppEvent.ToggleRemoteControl -> handleToggleRemoteControl(state.appState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleToggleRemoteControl(appState: AppState) {
|
||||
val enabled = !appState.isRemoteControlEnabled
|
||||
if (enabled) appDataRepository.appState.setRemoteKey(UUID.randomUUID().toString())
|
||||
appDataRepository.appState.setIsRemoteControlEnabled(enabled)
|
||||
}
|
||||
|
||||
private fun startCollectingLogs() {
|
||||
viewModelScope.launch {
|
||||
logReader.bufferedLogs
|
||||
@@ -290,7 +298,7 @@ constructor(
|
||||
true,
|
||||
)
|
||||
}
|
||||
if (!state.generalState.isBatteryOptimizationDisableShown) return@withLock requestBatteryPermission(true)
|
||||
if (!state.appState.isBatteryOptimizationDisableShown) return@withLock requestBatteryPermission(true)
|
||||
serviceManager.toggleAutoTunnel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,4 +62,5 @@ sealed class AppEvent {
|
||||
data class SetSelectedTunnel(val tunnel: TunnelConf?) : AppEvent()
|
||||
data object VpnPermissionRequested : AppEvent()
|
||||
data class AppReadyCheck(val tunnels: List<TunnelConf>) : AppEvent()
|
||||
data object ToggleRemoteControl : AppEvent()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<resources>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="app_permission_title">WG Tunnel Control Bridge</string>
|
||||
<string name="app_permission_description">Control tunnels and auto-tunnel features.</string>
|
||||
<string name="vpn_channel_id" translatable="false">VPN Channel</string>
|
||||
<string name="vpn_channel_name">VPN Notification Channel</string>
|
||||
<string name="github_url" translatable="false">https://github.com/zaneschepke/wgtunnel/issues</string>
|
||||
@@ -238,6 +240,7 @@
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="tunnel_error_template">Tunnel failed with: %1$s</string>
|
||||
<string name="wifi_name_template">Active: %1$s</string>
|
||||
<string name="remote_key_template">Key: %1$s</string>
|
||||
<string name="config_error">config error</string>
|
||||
<string name="dns_resolve_error">dns resolution error</string>
|
||||
<string name="invalid_config_error">invalid_config_error</string>
|
||||
@@ -253,4 +256,5 @@
|
||||
<string name="bio_not_created">Biometrics not created</string>
|
||||
<string name="bio_update_required">Biometric security update required</string>
|
||||
<string name="tunnel_starting">Tunnel starting</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
object Constants {
|
||||
const val VERSION_NAME = "3.8.0"
|
||||
const val VERSION_NAME = "3.8.1"
|
||||
const val JVM_TARGET = "17"
|
||||
const val VERSION_CODE = 38000
|
||||
const val VERSION_CODE = 38100
|
||||
const val TARGET_SDK = 35
|
||||
const val MIN_SDK = 26
|
||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
What's new:
|
||||
- New remote app intent integration
|
||||
- Copy active wifi name to clipboard
|
||||
- Fix for tunnel shutdown bug
|
||||
- Fix for DNS default server bug, preferring phone DNS
|
||||
- Fix for Ipv6 static peer host configs
|
||||
@@ -1,7 +1,7 @@
|
||||
[versions]
|
||||
accompanist = "0.37.2"
|
||||
activityCompose = "1.10.1"
|
||||
amneziawgAndroid = "1.3.2"
|
||||
amneziawgAndroid = "1.3.4"
|
||||
androidx-junit = "1.2.1"
|
||||
appcompat = "1.7.0"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
@@ -19,7 +19,7 @@ navigationCompose = "2.8.9"
|
||||
pinLockCompose = "1.0.4"
|
||||
roomVersion = "2.6.1"
|
||||
timber = "5.0.1"
|
||||
tunnel = "1.2.8"
|
||||
tunnel = "1.2.10"
|
||||
androidGradlePlugin = "8.8.0-alpha05"
|
||||
kotlin = "2.1.20"
|
||||
ksp = "2.1.20-1.0.32"
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4
|
||||
0
|
||||
Reference in New Issue
Block a user