mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eeccc71469 | |||
| 0e64bbb4e1 |
@@ -17,7 +17,7 @@ android {
|
|||||||
|
|
||||||
val versionMajor = 2
|
val versionMajor = 2
|
||||||
val versionMinor = 3
|
val versionMinor = 3
|
||||||
val versionPatch = 5
|
val versionPatch = 7
|
||||||
val versionBuild = 0
|
val versionBuild = 0
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@@ -89,7 +89,7 @@ dependencies {
|
|||||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
|
|
||||||
// compose navigation
|
// compose navigation
|
||||||
implementation("androidx.navigation:navigation-compose:2.7.0")
|
implementation("androidx.navigation:navigation-compose:2.7.1")
|
||||||
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
|
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
|
||||||
|
|
||||||
// hilt
|
// hilt
|
||||||
|
|||||||
@@ -64,6 +64,20 @@
|
|||||||
android:foregroundServiceType="remoteMessaging"
|
android:foregroundServiceType="remoteMessaging"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
</service>
|
</service>
|
||||||
|
<service
|
||||||
|
android:exported="true"
|
||||||
|
android:name=".service.TunnelControlTile"
|
||||||
|
android:icon="@drawable/shield"
|
||||||
|
android:label="WG Tunnel"
|
||||||
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||||
|
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
|
||||||
|
android:value="true" />
|
||||||
|
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||||
|
android:value="true" />
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name=".service.foreground.WireGuardTunnelService"
|
android:name=".service.foreground.WireGuardTunnelService"
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.service
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.service.quicksettings.Tile
|
||||||
|
import android.service.quicksettings.TileService
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardConnectivityWatcherService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class TunnelControlTile : TileService() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var settingsRepo : Repository<Settings>
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var configRepo : Repository<TunnelConfig>
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var vpnService : VpnService
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.Main);
|
||||||
|
|
||||||
|
private lateinit var job : Job
|
||||||
|
|
||||||
|
override fun onStartListening() {
|
||||||
|
job = scope.launch {
|
||||||
|
updateTileState()
|
||||||
|
}
|
||||||
|
super.onStartListening()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTileAdded() {
|
||||||
|
super.onTileAdded()
|
||||||
|
qsTile.contentDescription = this.resources.getString(R.string.toggle_vpn)
|
||||||
|
scope.launch {
|
||||||
|
updateTileState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTileRemoved() {
|
||||||
|
super.onTileRemoved()
|
||||||
|
cancelJob()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick() {
|
||||||
|
super.onClick()
|
||||||
|
unlockAndRun {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val tunnel = determineTileTunnel();
|
||||||
|
if(tunnel != null) {
|
||||||
|
attemptWatcherServiceToggle(tunnel.toString())
|
||||||
|
if(vpnService.getState() == Tunnel.State.UP) {
|
||||||
|
stopTunnelService();
|
||||||
|
} else {
|
||||||
|
startTunnelService(tunnel.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e : Exception) {
|
||||||
|
Timber.e(e.message)
|
||||||
|
} finally {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun determineTileTunnel() : TunnelConfig? {
|
||||||
|
var tunnelConfig : TunnelConfig? = null;
|
||||||
|
val settings = settingsRepo.getAll()
|
||||||
|
if (!settings.isNullOrEmpty()) {
|
||||||
|
val setting = settings.first()
|
||||||
|
tunnelConfig = if (setting.defaultTunnel != null) {
|
||||||
|
TunnelConfig.from(setting.defaultTunnel!!);
|
||||||
|
} else {
|
||||||
|
val config = configRepo.getAll()?.first();
|
||||||
|
config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tunnelConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopTunnelService() {
|
||||||
|
ServiceTracker.actionOnService(
|
||||||
|
Action.STOP, this.applicationContext,
|
||||||
|
WireGuardTunnelService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startTunnelService(tunnelConfig : String) {
|
||||||
|
ServiceTracker.actionOnService(
|
||||||
|
Action.START, this.applicationContext,
|
||||||
|
WireGuardTunnelService::class.java,
|
||||||
|
mapOf(this.applicationContext.resources.
|
||||||
|
getString(R.string.tunnel_extras_key) to
|
||||||
|
tunnelConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startWatcherService(tunnelConfig : String) {
|
||||||
|
ServiceTracker.actionOnService(
|
||||||
|
Action.START, this,
|
||||||
|
WireGuardConnectivityWatcherService::class.java, mapOf(this.resources.
|
||||||
|
getString(R.string.tunnel_extras_key) to
|
||||||
|
tunnelConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopWatcherService() {
|
||||||
|
ServiceTracker.actionOnService(
|
||||||
|
Action.STOP, this,
|
||||||
|
WireGuardConnectivityWatcherService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
|
||||||
|
scope.launch {
|
||||||
|
val settings = settingsRepo.getAll()
|
||||||
|
if (!settings.isNullOrEmpty()) {
|
||||||
|
val setting = settings.first()
|
||||||
|
if(setting.isAutoTunnelEnabled) {
|
||||||
|
when(ServiceTracker.getServiceState( this@TunnelControlTile,
|
||||||
|
WireGuardConnectivityWatcherService::class.java,)) {
|
||||||
|
ServiceState.STARTED -> stopWatcherService()
|
||||||
|
ServiceState.STOPPED -> startWatcherService(tunnelConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val config = determineTileTunnel();
|
||||||
|
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
|
||||||
|
qsTile.updateTile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTileDescription(description : String) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
qsTile.subtitle = description
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
qsTile.stateDescription = description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelJob() {
|
||||||
|
if(this::job.isInitialized) {
|
||||||
|
job.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-3
@@ -4,10 +4,7 @@ import android.app.Service
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import com.zaneschepke.wireguardautotunnel.repository.Repository
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
|
||||||
open class ForegroundService : Service() {
|
open class ForegroundService : Service() {
|
||||||
|
|||||||
+2
@@ -24,6 +24,7 @@ object ServiceTracker {
|
|||||||
|
|
||||||
fun <T : Service> actionOnService(action: Action, application: Application, cls : Class<T>, extras : Map<String,String>? = null) {
|
fun <T : Service> actionOnService(action: Action, application: Application, cls : Class<T>, extras : Map<String,String>? = null) {
|
||||||
if (getServiceState(application, cls) == ServiceState.STOPPED && action == Action.STOP) return
|
if (getServiceState(application, cls) == ServiceState.STOPPED && action == Action.STOP) return
|
||||||
|
if (getServiceState(application, cls) == ServiceState.STARTED && action == Action.START) return
|
||||||
val intent = Intent(application, cls).also {
|
val intent = Intent(application, cls).also {
|
||||||
it.action = action.name
|
it.action = action.name
|
||||||
extras?.forEach {(k, v) ->
|
extras?.forEach {(k, v) ->
|
||||||
@@ -40,6 +41,7 @@ object ServiceTracker {
|
|||||||
|
|
||||||
fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
|
fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
|
||||||
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
|
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
|
||||||
|
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
|
||||||
val intent = Intent(context, cls).also {
|
val intent = Intent(context, cls).also {
|
||||||
it.action = action.name
|
it.action = action.name
|
||||||
extras?.forEach {(k, v) ->
|
extras?.forEach {(k, v) ->
|
||||||
|
|||||||
-1
@@ -85,7 +85,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cancelWatcherJob()
|
cancelWatcherJob()
|
||||||
stopVPN()
|
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -28,7 +28,6 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
|||||||
object : ConnectivityManager.NetworkCallback(
|
object : ConnectivityManager.NetworkCallback(
|
||||||
FLAG_INCLUDE_LOCATION_INFO
|
FLAG_INCLUDE_LOCATION_INFO
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
trySend(NetworkStatus.Available(network))
|
trySend(NetworkStatus.Available(network))
|
||||||
}
|
}
|
||||||
@@ -68,6 +67,8 @@ abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Contex
|
|||||||
}
|
}
|
||||||
val request = NetworkRequest.Builder()
|
val request = NetworkRequest.Builder()
|
||||||
.addTransportType(networkCapability)
|
.addTransportType(networkCapability)
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
.build()
|
.build()
|
||||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
||||||
|
|
||||||
|
|||||||
+1
@@ -30,6 +30,7 @@ class WireGuardTunnel @Inject constructor(private val backend : Backend,
|
|||||||
override val tunnelName get() = _tunnelName.asStateFlow()
|
override val tunnelName get() = _tunnelName.asStateFlow()
|
||||||
|
|
||||||
private val _state = MutableSharedFlow<Tunnel.State>(
|
private val _state = MutableSharedFlow<Tunnel.State>(
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||||
replay = 1)
|
replay = 1)
|
||||||
|
|
||||||
private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1,
|
private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1,
|
||||||
|
|||||||
+1
-1
@@ -11,5 +11,5 @@ data class Settings(
|
|||||||
var isTunnelOnMobileDataEnabled : Boolean = false,
|
var isTunnelOnMobileDataEnabled : Boolean = false,
|
||||||
var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
|
var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
|
||||||
var defaultTunnel : String? = null,
|
var defaultTunnel : String? = null,
|
||||||
var isAlwaysOnVpnEnabled : Boolean = false
|
var isAlwaysOnVpnEnabled : Boolean = false,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController, focusRequester = focusRequester)
|
MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController)
|
||||||
}
|
}
|
||||||
composable(Routes.Settings.name, enterTransition = {
|
composable(Routes.Settings.name, enterTransition = {
|
||||||
when (initialState.destination.route) {
|
when (initialState.destination.route) {
|
||||||
|
|||||||
+26
-10
@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.FileOpen
|
import androidx.compose.material.icons.filled.FileOpen
|
||||||
@@ -85,7 +86,6 @@ import kotlinx.coroutines.launch
|
|||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
viewModel: MainViewModel = hiltViewModel(), padding: PaddingValues,
|
viewModel: MainViewModel = hiltViewModel(), padding: PaddingValues,
|
||||||
focusRequester: FocusRequester,
|
|
||||||
snackbarHostState: SnackbarHostState, navController: NavController
|
snackbarHostState: SnackbarHostState, navController: NavController
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -241,9 +241,12 @@ fun MainScreen(
|
|||||||
.padding(padding)
|
.padding(padding)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize()
|
LazyColumn(
|
||||||
.nestedScroll(nestedScrollConnection),) {
|
modifier = Modifier.fillMaxSize()
|
||||||
items(tunnels.toList()) { tunnel ->
|
.nestedScroll(nestedScrollConnection),
|
||||||
|
) {
|
||||||
|
itemsIndexed(tunnels.toList()) { index, tunnel ->
|
||||||
|
val focusRequester = FocusRequester();
|
||||||
RowListItem(leadingIcon = Icons.Rounded.Circle,
|
RowListItem(leadingIcon = Icons.Rounded.Circle,
|
||||||
leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) {
|
leadingIconColor = if (tunnelName == tunnel.name) when (handshakeStatus) {
|
||||||
HandshakeStatus.HEALTHY -> mint
|
HandshakeStatus.HEALTHY -> mint
|
||||||
@@ -271,7 +274,7 @@ fun MainScreen(
|
|||||||
},
|
},
|
||||||
rowButton = {
|
rowButton = {
|
||||||
if (tunnel.id == selectedTunnel?.id) {
|
if (tunnel.id == selectedTunnel?.id) {
|
||||||
Row() {
|
Row {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}")
|
navController.navigate("${Routes.Config.name}/${selectedTunnel?.id}")
|
||||||
}) {
|
}) {
|
||||||
@@ -288,8 +291,10 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
if (WireGuardAutoTunnel.isRunningOnAndroidTv(context)) {
|
||||||
Row() {
|
Row {
|
||||||
IconButton(modifier = Modifier.focusRequester(focusRequester),onClick = {
|
IconButton(
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
onClick = {
|
||||||
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
|
navController.navigate("${Routes.Detail.name}/${tunnel.id}")
|
||||||
}) {
|
}) {
|
||||||
Icon(Icons.Rounded.Info, "Info")
|
Icon(Icons.Rounded.Info, "Info")
|
||||||
@@ -297,17 +302,28 @@ fun MainScreen(
|
|||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
|
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.showSnackBarMessage(context.resources.getString(R.string.turn_off_tunnel))
|
viewModel.showSnackBarMessage(
|
||||||
|
context.resources.getString(
|
||||||
|
R.string.turn_off_tunnel
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
navController.navigate("${Routes.Config.name}/${tunnel.id}")
|
navController.navigate("${Routes.Config.name}/${tunnel.id}")
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(Icons.Rounded.Edit, stringResource(id = R.string.edit))
|
Icon(
|
||||||
|
Icons.Rounded.Edit,
|
||||||
|
stringResource(id = R.string.edit)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
|
if (state == Tunnel.State.UP && tunnel.name == tunnelName)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.showSnackBarMessage(context.resources.getString(R.string.turn_off_tunnel))
|
viewModel.showSnackBarMessage(
|
||||||
|
context.resources.getString(
|
||||||
|
R.string.turn_off_tunnel
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
viewModel.onDelete(tunnel)
|
viewModel.onDelete(tunnel)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z"/>
|
||||||
|
</vector>
|
||||||
@@ -84,4 +84,6 @@
|
|||||||
<string name="detecting_location_services_disabled">Detecting Location Services disabled</string>
|
<string name="detecting_location_services_disabled">Detecting Location Services disabled</string>
|
||||||
<string name="precise_location_message">This feature requires precise location to access Wi-Fi SSID name. Please enable precise location here or in the app settings.</string>
|
<string name="precise_location_message">This feature requires precise location to access Wi-Fi SSID name. Please enable precise location here or in the app settings.</string>
|
||||||
<string name="request">Request</string>
|
<string name="request">Request</string>
|
||||||
|
<string name="toggle_vpn">Toggle VPN</string>
|
||||||
|
<string name="no_tunnel_available">No tunnels available</string>
|
||||||
</resources>
|
</resources>
|
||||||
+1
-1
@@ -13,7 +13,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "8.2.0-alpha15" apply false
|
id("com.android.application") version "8.2.0-beta01" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
|
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
|
||||||
id("com.google.dagger.hilt.android") version "2.44" apply false
|
id("com.google.dagger.hilt.android") version "2.44" apply false
|
||||||
kotlin("plugin.serialization") version "1.8.22" apply false
|
kotlin("plugin.serialization") version "1.8.22" apply false
|
||||||
|
|||||||
Reference in New Issue
Block a user