Compare commits

..

12 Commits

Author SHA1 Message Date
Zane Schepke 21e56cda80 chore: bump app version with notes 2025-07-25 14:58:37 -04:00
Zane Schepke b5196fbf01 fix: android tv sorting bug, improve hover visibility 2025-07-23 02:09:34 -04:00
Zane Schepke e46fe93ae0 fix: improve network detection reliability, permission change detection
#848
2025-07-22 17:28:18 -04:00
Zane Schepke 872ff83a12 feat!: tunnel sorting
#847
closes #846
closes #299
2025-07-17 11:45:46 -04:00
Zane Schepke 5563292a87 build(deps): bump upstream libraries to latest versions after sync 2025-07-13 13:29:26 -04:00
Zane Schepke 8ba760a5ff refactor: auto expand tunnel stats on active 2025-07-11 17:09:52 -04:00
Zane Schepke d431c2d39f chore: bump deps, fix localization sync duplicates 2025-07-11 14:07:05 -04:00
Zane Schepke 33437ab237 chore: fix weblate sync 2025-07-11 13:38:03 -04:00
Zane Schepke 4a432d2bb7 refactor: remove rudundant pt 2025-07-11 13:22:08 -04:00
Zane Schepke 3df972d031 feat(lang): weblate localization changes (#857)
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: kometchtech <kometch@gmail.com>
Co-authored-by: 翻譯得真好下次別翻了 <x86_64-pc-linux-gnu@proton.me>
Co-authored-by: solokot <solokot@gmail.com>
Co-authored-by: Kachelkaiser <kachelkaiser@htpst.de>
Co-authored-by: catelixor <catelixor+weblate@proton.me>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Faisal Gull <mail.faisalrehman.345@gmail.com>
Co-authored-by: vm <varga.m007@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: sgauthiertremblay <info@sgauthiertremblay.dev>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: Valentin <velentin.s@yandex.ru>
Co-authored-by: adkostatt <adkostatt@gmail.com>
Co-authored-by: VertekPlus <vertekplus@users.noreply.hosted.weblate.org>
Co-authored-by: Jasper <jasper@ennik.com>
Co-authored-by: Tommaso <mrduckhunt@users.noreply.hosted.weblate.org>
Co-authored-by: dct <dct@trnh.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: angrybb <lijadolija@gmail.com>
Co-authored-by: Saratoga79 <ordizi79@gmail.com>
Co-authored-by: Deleted User <noreply+48943@weblate.org>
Co-authored-by: François-Xavier Choinière <fx@efficks.com>
Co-authored-by: Noureddine <noureddinex@protonmail.com>
Co-authored-by: Hamed Ap <hamed.ap1366@gmail.com>
Co-authored-by: igor <igor.lachaud@aol.fr>
Co-authored-by: EESF-2 <eesf-2@users.noreply.hosted.weblate.org>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Jan-Erik Moen <jemoen@gmail.com>
Co-authored-by: teemue <eemil.koivula@gmail.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Andras <andras0602@hotmail.com>
2025-07-11 13:00:24 -04:00
Zane Schepke 8b828cca55 fix: nightly installer permission bug 2025-07-06 04:13:59 -04:00
Zane Schepke a223289949 feat: add shizuku support (#852) 2025-07-05 20:49:02 -04:00
87 changed files with 1727 additions and 738 deletions
+2
View File
@@ -223,6 +223,8 @@ dependencies {
// shizuku
implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
implementation(libs.reorderable)
}
tasks.register<Copy>("copyLicenseeJsonToAssets") {
@@ -0,0 +1,302 @@
{
"formatVersion": 1,
"database": {
"version": 18,
"identityHash": "505728bad740c12bab998a066b569333",
"entities": [
{
"tableName": "Settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT false, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT false, `is_vpn_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `is_lan_on_kill_switch_enabled` INTEGER NOT NULL DEFAULT false, `debounce_delay_seconds` INTEGER NOT NULL DEFAULT 3, `is_disable_kill_switch_on_trusted_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT false, `split_tunnel_apps` TEXT NOT NULL DEFAULT '', `wifi_detection_method` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnMobileDataEnabled",
"columnName": "is_tunnel_on_mobile_data_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlwaysOnVpnEnabled",
"columnName": "is_always_on_vpn_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isShortcutsEnabled",
"columnName": "is_shortcuts_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelEnabled",
"columnName": "is_kernel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isMultiTunnelEnabled",
"columnName": "is_multi_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isAmneziaEnabled",
"columnName": "is_amnezia_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isVpnKillSwitchEnabled",
"columnName": "is_vpn_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isKernelKillSwitchEnabled",
"columnName": "is_kernel_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isLanOnKillSwitchEnabled",
"columnName": "is_lan_on_kill_switch_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "debounceDelaySeconds",
"columnName": "debounce_delay_seconds",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "3"
},
{
"fieldPath": "isDisableKillSwitchOnTrustedEnabled",
"columnName": "is_disable_kill_switch_on_trusted_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "splitTunnelApps",
"columnName": "split_tunnel_apps",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "wifiDetectionMethod",
"columnName": "wifi_detection_method",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "TunnelConfig",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '', `is_Active` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `ping_interval` INTEGER DEFAULT null, `ping_cooldown` INTEGER DEFAULT null, `ping_ip` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "wgQuick",
"columnName": "wg_quick",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isMobileDataTunnel",
"columnName": "is_mobile_data_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "amQuick",
"columnName": "am_quick",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isActive",
"columnName": "is_Active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isPingEnabled",
"columnName": "is_ping_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingInterval",
"columnName": "ping_interval",
"affinity": "INTEGER",
"defaultValue": "null"
},
{
"fieldPath": "pingCooldown",
"columnName": "ping_cooldown",
"affinity": "INTEGER",
"defaultValue": "null"
},
{
"fieldPath": "pingIp",
"columnName": "ping_ip",
"affinity": "TEXT",
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv4Preferred",
"columnName": "is_ipv4_preferred",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_TunnelConfig_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)"
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '505728bad740c12bab998a066b569333')"
]
}
}
@@ -1,9 +1,7 @@
package com.zaneschepke.wireguardautotunnel
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Build
import android.os.Bundle
@@ -27,7 +25,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -54,6 +51,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.disclosure.Loca
import com.zaneschepke.wireguardautotunnel.ui.screens.main.MainScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.autotunnel.TunnelAutoTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.config.ConfigScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.sort.SortScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel.SplitTunnelScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.main.tunneloptions.TunnelOptionsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.pin.PinLockScreen
@@ -74,7 +72,6 @@ import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.system.exitProcess
import org.amnezia.awg.backend.GoBackend.VpnService
import timber.log.Timber
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@@ -322,6 +319,7 @@ class MainActivity : AppCompatActivity() {
)
}
}
composable<Route.Sort> { SortScreen(appUiState, viewModel) }
}
}
}
@@ -330,22 +328,4 @@ class MainActivity : AppCompatActivity() {
}
}
}
override fun onResume() {
super.onResume()
checkPermissionAndNotify()
}
private fun checkPermissionAndNotify() {
val hasLocation =
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
if (lastLocationPermissionState != hasLocation) {
Timber.d("Location permission changed to: $hasLocation")
if (hasLocation) {
networkMonitor.sendLocationPermissionsGrantedBroadcast()
}
lastLocationPermissionState = hasLocation
}
}
}
@@ -8,7 +8,6 @@ import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
@@ -217,9 +216,8 @@ class TunnelForegroundService : LifecycleService() {
}
private suspend fun startNetworkMonitorJob() {
networkMonitor.networkStatusFlow.flowOn(ioDispatcher).collectLatest { status ->
val isAvailable = status !is NetworkStatus.Disconnected
isNetworkConnected.value = isAvailable
networkMonitor.connectivityStateFlow.flowOn(ioDispatcher).collectLatest { status ->
isNetworkConnected.value = status.hasConnectivity()
Timber.d("Network available: $status")
}
}
@@ -7,8 +7,8 @@ import android.os.PowerManager
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.networkmonitor.ConnectivityState
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
@@ -158,20 +158,13 @@ class AutoTunnelService : LifecycleService() {
}
}
private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
private fun buildNetworkState(connectivityState: ConnectivityState): NetworkState {
return with(autoTunnelStateFlow.value.networkState) {
val wifiName =
when (networkStatus) {
is NetworkStatus.Connected -> {
networkStatus.wifiSsid
}
else -> null
}
copy(
isWifiConnected = networkStatus.wifiConnected,
isMobileDataConnected = networkStatus.cellularConnected,
isEthernetConnected = networkStatus.ethernetConnected,
wifiName = wifiName,
isWifiConnected = connectivityState.wifiState.connected,
isMobileDataConnected = connectivityState.cellularConnected,
isEthernetConnected = connectivityState.ethernetConnected,
wifiName = connectivityState.wifiState.ssid,
)
}
}
@@ -189,7 +182,7 @@ class AutoTunnelService : LifecycleService() {
old.isKernelEnabled == new.isKernelEnabled
} // Only emit when isKernelEnabled changes
.flatMapLatest {
networkMonitor.networkStatusFlow.flowOn(ioDispatcher).map {
networkMonitor.connectivityStateFlow.flowOn(ioDispatcher).map {
buildNetworkState(it)
}
}
@@ -13,7 +13,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
@Database(
entities = [Settings::class, TunnelConfig::class],
version = 17,
version = 18,
autoMigrations =
[
AutoMigration(from = 1, to = 2),
@@ -32,6 +32,7 @@ import com.zaneschepke.wireguardautotunnel.data.entity.TunnelConfig
AutoMigration(from = 14, to = 15),
AutoMigration(from = 15, to = 16),
AutoMigration(from = 16, to = 17, spec = WifiDetectionMigration::class),
AutoMigration(from = 17, to = 18),
],
exportSchema = true,
)
@@ -46,5 +46,6 @@ interface TunnelConfigDao {
@Query("SELECT * FROM TUNNELCONFIG WHERE is_mobile_data_tunnel=1")
suspend fun findByMobileDataTunnel(): TunnelConfigs
@Query("SELECT * FROM tunnelconfig") fun getAllFlow(): Flow<MutableList<TunnelConfig>>
@Query("SELECT * FROM tunnelconfig ORDER BY position ASC")
fun getAllFlow(): Flow<MutableList<TunnelConfig>>
}
@@ -24,9 +24,10 @@ data class TunnelConfig(
@ColumnInfo(name = "ping_cooldown", defaultValue = "null") val pingCooldown: Long? = null,
@ColumnInfo(name = "ping_ip", defaultValue = "null") var pingIp: String? = null,
@ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false")
var isEthernetTunnel: Boolean = false,
val isEthernetTunnel: Boolean = false,
@ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true")
var isIpv4Preferred: Boolean = true,
val isIpv4Preferred: Boolean = true,
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
) {
companion object {
@@ -21,6 +21,7 @@ object TunnelConfigMapper {
pingIp,
isEthernetTunnel,
isIpv4Preferred,
position,
)
}
}
@@ -42,6 +43,7 @@ object TunnelConfigMapper {
pingIp,
isEthernetTunnel,
isIpv4Preferred,
position,
)
}
}
@@ -26,6 +26,7 @@ data class TunnelConf(
val pingIp: String? = null,
val isEthernetTunnel: Boolean = false,
val isIpv4Preferred: Boolean = true,
val position: Int = 0,
@Transient private var stateChangeCallback: ((Any) -> Unit)? = null,
) : Tunnel, org.amnezia.awg.backend.Tunnel {
@@ -45,4 +45,6 @@ sealed class Route {
@Serializable data class TunnelAutoTunnel(val id: Int) : Route()
@Serializable data object Logs : Route()
@Serializable data object Sort : Route()
}
@@ -3,68 +3,41 @@ package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ExpandingRowListItem(
leading: @Composable () -> Unit,
text: String,
onHold: () -> Unit,
onClick: () -> Unit,
onDoubleClick: () -> Unit,
trailing: @Composable () -> Unit,
isSelected: Boolean,
expanded: (@Composable () -> Unit)?,
expanded: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
val isTv = LocalIsAndroidTV.current
val haptic = LocalHapticFeedback.current
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier =
Modifier.animateContentSize()
modifier
.animateContentSize()
.clip(RoundedCornerShape(8.dp))
.background(
if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
else Color.Transparent
)
.then(
if (!isTv) {
Modifier.combinedClickable(
interactionSource = interactionSource,
indication = ripple(),
onClick = onClick,
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onHold()
},
onDoubleClick = onDoubleClick,
)
} else Modifier
)
) {
Column {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).height(48.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
@@ -84,7 +57,7 @@ fun ExpandingRowListItem(
}
trailing()
}
expanded?.invoke()
expanded()
}
}
}
@@ -10,7 +10,6 @@ import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -68,53 +67,42 @@ fun BottomNavbar(appUiState: AppUiState) {
onClick = { navController.goFromRoot(Route.Support) },
),
)
// Define ripple configuration based on platform
val rippleConfiguration =
if (isTv) {
RippleConfiguration()
} else {
null
}
// Apply ripple configuration only if needed
CompositionLocalProvider(LocalRippleConfiguration provides rippleConfiguration) {
NavigationBar(containerColor = MaterialTheme.colorScheme.surface) {
items.forEach { item ->
val isSelected = navBackStackEntry.isCurrentRoute(item.route::class)
val interactionSource = remember { MutableInteractionSource() }
NavigationBar(containerColor = MaterialTheme.colorScheme.surface) {
items.forEach { item ->
val isSelected = navBackStackEntry.isCurrentRoute(item.route::class)
val interactionSource = remember { MutableInteractionSource() }
NavigationBarItem(
icon = {
if (item.active) {
BadgedBox(
badge = {
Badge(
modifier =
Modifier.offset(x = 8.dp, y = (-8).dp).size(6.dp),
containerColor = SilverTree,
)
}
) {
Icon(imageVector = item.icon, contentDescription = item.name)
NavigationBarItem(
icon = {
if (item.active) {
BadgedBox(
badge = {
Badge(
modifier = Modifier.offset(x = 8.dp, y = (-8).dp).size(6.dp),
containerColor = SilverTree,
)
}
} else {
) {
Icon(imageVector = item.icon, contentDescription = item.name)
}
},
onClick = { navController.goFromRoot(item.route) },
selected = isSelected,
enabled = true,
label = null,
alwaysShowLabel = false,
colors =
NavigationBarItemDefaults.colors(
selectedIconColor = MaterialTheme.colorScheme.primary,
unselectedIconColor = MaterialTheme.colorScheme.onBackground,
indicatorColor = Color.Transparent,
),
interactionSource = interactionSource,
)
}
} else {
Icon(imageVector = item.icon, contentDescription = item.name)
}
},
onClick = { navController.goFromRoot(item.route) },
selected = isSelected,
enabled = true,
label = null,
alwaysShowLabel = false,
colors =
NavigationBarItemDefaults.colors(
selectedIconColor = MaterialTheme.colorScheme.primary,
unselectedIconColor = MaterialTheme.colorScheme.onBackground,
indicatorColor = Color.Transparent,
),
interactionSource = interactionSource,
)
}
}
}
@@ -4,6 +4,7 @@ import android.os.Build
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Sort
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -11,6 +12,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
@@ -28,6 +30,7 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
@Composable
fun currentNavBackStackEntryAsNavBarState(
@@ -60,35 +63,40 @@ fun currentNavBackStackEntryAsNavBarState(
Row {
if (selectedCount == 0) {
val showSort = remember(uiState.tunnels) { uiState.tunnels.size > 1 }
if (showSort)
ActionIconButton(Icons.AutoMirrored.Rounded.Sort, R.string.sort) {
navController.navigate(Route.Sort)
}
ActionIconButton(Icons.Rounded.Add, R.string.add_tunnel) {
viewModel.handleEvent(
AppEvent.SetBottomSheet(AppViewState.BottomSheet.IMPORT_TUNNELS)
)
}
} else {
ActionIconButton(Icons.Rounded.SelectAll, R.string.select_all) {
viewModel.handleEvent(AppEvent.ToggleSelectAllTunnels)
}
// due to permissions, and SAF issues on TV, not support less than Android 10 on
// Android TV for file exports
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ActionIconButton(Icons.Rounded.Download, R.string.download) {
viewModel.handleEvent(
AppEvent.SetBottomSheet(AppViewState.BottomSheet.EXPORT_TUNNELS)
)
}
return@Row
}
ActionIconButton(Icons.Rounded.SelectAll, R.string.select_all) {
viewModel.handleEvent(AppEvent.ToggleSelectAllTunnels)
}
// due to permissions, and SAF issues on TV, not support less than Android 10 on
// Android TV for file exports
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ActionIconButton(Icons.Rounded.Download, R.string.download) {
viewModel.handleEvent(
AppEvent.SetBottomSheet(AppViewState.BottomSheet.EXPORT_TUNNELS)
)
}
}
if (selectedCount == 1) {
ActionIconButton(Icons.Rounded.CopyAll, R.string.copy) {
viewModel.handleEvent(AppEvent.CopySelectedTunnel)
}
if (selectedCount == 1) {
ActionIconButton(Icons.Rounded.CopyAll, R.string.copy) {
viewModel.handleEvent(AppEvent.CopySelectedTunnel)
}
}
if (showDelete) {
ActionIconButton(Icons.Rounded.Delete, R.string.delete_tunnel) {
viewModel.handleEvent(AppEvent.SetShowModal(AppViewState.ModalType.DELETE))
}
if (showDelete) {
ActionIconButton(Icons.Rounded.Delete, R.string.delete_tunnel) {
viewModel.handleEvent(AppEvent.SetShowModal(AppViewState.ModalType.DELETE))
}
}
}
@@ -212,6 +220,25 @@ fun currentNavBackStackEntryAsNavBarState(
route = Route.Support,
)
backStackEntry.isCurrentRoute(Route.Sort::class) -> {
NavBarState(
showTop = true,
showBottom = true,
topTitle = { Text(stringResource(R.string.sort)) },
topTrailing = {
Row {
ActionIconButton(Icons.Rounded.SortByAlpha, R.string.sort) {
viewModel.handleUiEvent(UiEvent.SortTunnels)
}
ActionIconButton(Icons.Rounded.Save, R.string.save) {
viewModel.handleEvent(AppEvent.InvokeScreenAction)
}
}
},
route = Route.Sort,
)
}
backStackEntry.isCurrentRoute(Route.License::class) -> {
NavBarState(
showTop = true,
@@ -40,8 +40,8 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
},
description = {
val cellularActive =
remember(uiState.networkStatus) {
uiState.networkStatus?.cellularConnected ?: false
remember(uiState.connectivityState) {
uiState.connectivityState?.cellularConnected ?: false
}
Text(
text =
@@ -77,8 +77,8 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
},
description = {
val ethernetActive =
remember(uiState.networkStatus) {
uiState.networkStatus?.ethernetConnected ?: false
remember(uiState.connectivityState) {
uiState.connectivityState?.ethernetConnected ?: false
}
Text(
text =
@@ -18,7 +18,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
@@ -68,11 +67,12 @@ fun WifiTunnelingItems(
},
description = {
val wifiInfo by
remember(uiState.networkStatus) {
remember(uiState.connectivityState) {
derivedStateOf {
(uiState.networkStatus as? NetworkStatus.Connected)
?.takeIf { it.wifiConnected }
.let { Pair(it?.wifiSsid, it?.securityType) }
uiState.connectivityState
?.wifiState
?.takeIf { it.connected }
.let { Pair(it?.ssid, it?.securityType) }
}
}
val (wifiName, securityType) = wifiInfo
@@ -1,7 +1,9 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -9,6 +11,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.rememberOverscrollEffect
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
@@ -25,8 +28,6 @@ import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import java.text.Collator
import java.util.*
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -40,17 +41,10 @@ fun TunnelList(
val isTv = LocalIsAndroidTV.current
val context = LocalContext.current
val navController = LocalNavController.current
val collator = Collator.getInstance(Locale.getDefault())
val sortedTunnels =
remember(appUiState.tunnels) {
appUiState.tunnels.sortedWith(
compareBy(
// primary tunnel first
{ !it.isPrimaryTunnel },
{ collator.compare(it.tunName, "") },
)
)
}
val lazyListState = rememberLazyListState()
val sortedTunnels = remember(appUiState.tunnels) { appUiState.tunnels.sortedBy { it.position } }
LazyColumn(
horizontalAlignment = Alignment.Start,
@@ -59,7 +53,7 @@ fun TunnelList(
modifier
.pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput }
.overscroll(rememberOverscrollEffect()),
state = rememberLazyListState(0, appUiState.tunnels.count()),
state = lazyListState,
userScrollEnabled = true,
reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(),
@@ -75,26 +69,36 @@ fun TunnelList(
val selected = remember(selectedTunnels) { selectedTunnels.any { it.id == tunnel.id } }
TunnelRowItem(
state = tunnelState,
expanded = appUiState.appState.expandedTunnelIds.contains(tunnel.id),
isSelected = selected,
tunnel = tunnel,
tunnelState = tunnelState,
onClick = {
if (selectedTunnels.isNotEmpty() && !isTv) {
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
} else {
navController.navigate(Route.TunnelOptions(tunnel.id))
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
}
},
onDoubleClick = {
viewModel.handleEvent(AppEvent.ToggleTunnelStatsExpanded(tunnel.id))
onTvClick = {
navController.navigate(Route.TunnelOptions(tunnel.id))
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
},
onToggleSelectedTunnel = {
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(it))
},
onSwitchClick = { checked -> onToggleTunnel(tunnel, checked) },
isTv = isTv,
modifier =
if (!isTv)
Modifier.combinedClickable(
onClick = {
if (selectedTunnels.isNotEmpty()) {
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
} else {
navController.navigate(Route.TunnelOptions(tunnel.id))
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
}
},
onLongClick = {
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
},
interactionSource = remember { MutableInteractionSource() },
indication = null,
)
else Modifier,
)
}
}
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.SettingsEthernet
import androidx.compose.material.icons.rounded.Smartphone
@@ -22,6 +21,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
@@ -32,14 +32,13 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
fun TunnelRowItem(
state: TunnelState,
isSelected: Boolean,
expanded: Boolean,
tunnel: TunnelConf,
tunnelState: TunnelState,
onClick: () -> Unit,
onDoubleClick: () -> Unit,
onTvClick: () -> Unit,
onToggleSelectedTunnel: (TunnelConf) -> Unit,
onSwitchClick: (Boolean) -> Unit,
isTv: Boolean,
modifier: Modifier = Modifier,
) {
val leadingIconColor =
remember(state) {
@@ -78,13 +77,10 @@ fun TunnelRowItem(
}
},
text = tunnel.tunName,
onHold = { if (!isTv) onToggleSelectedTunnel(tunnel) },
onClick = { if (!isTv) onClick() },
onDoubleClick = { if (!isTv) onDoubleClick() },
expanded = {
if (expanded) {
if (tunnelState.status != TunnelStatus.Down) {
TunnelStatisticsRow(tunnelState.statistics, tunnel)
} else null
}
},
trailing = {
Row(
@@ -92,13 +88,7 @@ fun TunnelRowItem(
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
) {
if (isTv) {
IconButton(onClick = onDoubleClick) {
Icon(
Icons.Rounded.KeyboardArrowDown,
contentDescription = stringResource(R.string.info),
)
}
IconButton(onClick = onClick) {
IconButton(onClick = onTvClick) {
Icon(
Icons.Rounded.Settings,
contentDescription = stringResource(R.string.settings),
@@ -109,5 +99,6 @@ fun TunnelRowItem(
}
},
isSelected = isSelected,
modifier = modifier,
)
}
@@ -1,10 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
import androidx.compose.foundation.layout.Arrangement
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.animation.AnimatedVisibility
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -97,15 +94,17 @@ fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConf: TunnelConf) {
)
}
if (endpoint != null) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
) {
Text(
"endpoint: $endpoint",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
AnimatedVisibility(visible = true) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
) {
Text(
stringResource(R.string.endpoint).lowercase() + ": $endpoint",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
}
}
}
}
@@ -0,0 +1,168 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.sort
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.util.extensions.isSortedBy
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
import sh.calvin.reorderable.DragGestureDetector
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
@Composable
fun SortScreen(appUiState: AppUiState, viewModel: AppViewModel) {
val hapticFeedback = LocalHapticFeedback.current
val isTv = LocalIsAndroidTV.current
var sortAscending by remember { mutableStateOf<Boolean?>(null) }
var sortedTunnels by remember { mutableStateOf(appUiState.tunnels.sortedBy { it.position }) }
LaunchedEffect(Unit) {
viewModel.uiEvent.collect { uiEvent ->
when (uiEvent) {
UiEvent.SortTunnels -> {
sortAscending =
when (sortAscending) {
null -> !sortedTunnels.isSortedBy { it.name }
true -> false
false -> null
}
sortedTunnels =
when (sortAscending) {
true -> sortedTunnels.sortedBy { it.name }
false -> sortedTunnels.sortedByDescending { it.name }
null -> sortedTunnels.sortedBy { it.position }
}
}
}
}
}
LaunchedEffect(Unit) {
viewModel.handleEvent(
AppEvent.SetScreenAction {
viewModel.handleEvent(
AppEvent.SaveAllConfigs(
sortedTunnels.mapIndexed { index, conf -> conf.copy(position = index) }
)
)
viewModel.handleEvent(AppEvent.PopBackStack(true))
}
)
}
val lazyListState = rememberLazyListState()
val reorderableLazyListState =
rememberReorderableLazyListState(
lazyListState,
scrollThresholdPadding = WindowInsets.systemBars.asPaddingValues(),
) { from, to ->
sortedTunnels =
sortedTunnels.toMutableList().apply { add(to.index, removeAt(from.index)) }
hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
}
LazyColumn(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top),
modifier =
Modifier.pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput }
.overscroll(rememberOverscrollEffect())
.padding(horizontal = 16.dp, vertical = 24.dp),
state = lazyListState,
userScrollEnabled = true,
reverseLayout = false,
flingBehavior = ScrollableDefaults.flingBehavior(),
) {
itemsIndexed(sortedTunnels, key = { _, tunnel -> tunnel.id }) { index, tunnel ->
ReorderableItem(reorderableLazyListState, tunnel.id) { isDragging ->
ExpandingRowListItem(
leading = {},
text = tunnel.name,
trailing = {
if (!isTv)
Icon(
Icons.Default.DragHandle,
stringResource(
com.zaneschepke.wireguardautotunnel.R.string.drag_handle
),
)
else
Row {
IconButton(
onClick = {
sortedTunnels =
sortedTunnels.toMutableList().apply {
add(index - 1, removeAt(index))
}
},
enabled = index != 0,
) {
Icon(
Icons.Default.ArrowUpward,
stringResource(
com.zaneschepke.wireguardautotunnel.R.string.move_up
),
)
}
IconButton(
onClick = {
sortedTunnels =
sortedTunnels.toMutableList().apply {
add(index + 1, removeAt(index))
}
},
enabled = index != sortedTunnels.count() - 1,
) {
Icon(
Icons.Default.ArrowDownward,
stringResource(R.string.move_down),
)
}
}
},
isSelected = isDragging,
expanded = {},
modifier =
Modifier.draggableHandle(
onDragStarted = {
hapticFeedback.performHapticFeedback(
HapticFeedbackType.GestureThresholdActivate
)
},
onDragStopped = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd)
},
dragGestureDetector = DragGestureDetector.LongPress,
),
)
}
}
}
}
@@ -1,6 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.state
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.networkmonitor.ConnectivityState
import com.zaneschepke.wireguardautotunnel.data.entity.GeneralState
import com.zaneschepke.wireguardautotunnel.data.mapper.GeneralStateMapper
import com.zaneschepke.wireguardautotunnel.domain.model.AppSettings
@@ -16,5 +16,5 @@ data class AppUiState(
val isAutoTunnelActive: Boolean = false,
val appConfigurationChange: Boolean = false,
val isAppLoaded: Boolean = false,
val networkStatus: NetworkStatus? = null,
val connectivityState: ConnectivityState? = null,
)
@@ -3,18 +3,17 @@ package com.zaneschepke.wireguardautotunnel.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
private val DarkColorScheme =
darkColorScheme(
@@ -49,9 +48,11 @@ enum class Theme {
DYNAMIC,
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composable () -> Unit) {
val context = LocalContext.current
val isTv = LocalIsAndroidTV.current
var isDark = isSystemInDarkTheme()
val autoTheme = if (isDark) DarkColorScheme else LightColorScheme
val colorScheme =
@@ -105,5 +106,22 @@ fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composabl
}
}
MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
// Make hover/ripple more obvious on TV
val rippleConfig =
if (isTv) {
RippleConfiguration(
color = colorScheme.outline.copy(alpha = 0.12f),
rippleAlpha =
RippleAlpha(
pressedAlpha = 0.7f,
focusedAlpha = 0.6f,
draggedAlpha = 0.9f,
hoveredAlpha = 0.3f,
),
)
} else null
CompositionLocalProvider(LocalRippleConfiguration provides rippleConfig) {
MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
}
}
@@ -24,3 +24,7 @@ typealias Packages = List<PackageInfo>
fun <T> MutableList<T>.addAllUnique(elements: Collection<T>, comparator: (T, T) -> Boolean) {
addAll(elements.filterNot { new -> this.any { existing -> comparator(existing, new) } })
}
fun <T, R : Comparable<R>> List<T>.isSortedBy(selector: (T) -> R): Boolean {
return zipWithNext().all { (a, b) -> selector(a) <= selector(b) }
}
@@ -10,8 +10,8 @@ import com.wireguard.android.util.RootShell
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.ConnectivityState
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
@@ -34,6 +34,7 @@ import com.zaneschepke.wireguardautotunnel.util.*
import com.zaneschepke.wireguardautotunnel.util.extensions.addAllUnique
import com.zaneschepke.wireguardautotunnel.util.extensions.withFirstState
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import com.zaneschepke.wireguardautotunnel.viewmodel.event.UiEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import java.io.IOException
import java.net.URL
@@ -79,6 +80,9 @@ constructor(
private val _appViewState = MutableStateFlow(AppViewState())
val appViewState = _appViewState.asStateFlow()
private val _uiEvent = MutableSharedFlow<UiEvent>()
val uiEvent: SharedFlow<UiEvent> = _uiEvent.asSharedFlow()
private val _logs = MutableStateFlow<List<LogMessage>>(emptyList())
val logs: StateFlow<List<LogMessage>> = _logs.asStateFlow()
private val maxLogSize = Constants.MAX_LOG_SIZE
@@ -90,14 +94,14 @@ constructor(
appDataRepository.appState.flow,
tunnelManager.activeTunnels,
serviceManager.autoTunnelService.map { it != null },
networkMonitor.networkStatusFlow,
networkMonitor.connectivityStateFlow,
) { array ->
val settings = array[0] as AppSettings
val tunnels = array[1] as List<TunnelConf>
val appState = array[2] as AppState
val activeTunnels = array[3] as Map<TunnelConf, TunnelState>
val autoTunnel = array[4] as Boolean
val network = array[5] as NetworkStatus
val network = array[5] as ConnectivityState
AppUiState(
appSettings = settings,
@@ -106,7 +110,7 @@ constructor(
appState = appState,
isAutoTunnelActive = autoTunnel,
isAppLoaded = true,
networkStatus = network,
connectivityState = network,
)
}
.stateIn(
@@ -126,6 +130,9 @@ constructor(
}
}
fun handleUiEvent(event: UiEvent) =
viewModelScope.launch(mainDispatcher) { _uiEvent.emit(event) }
fun handleEvent(event: AppEvent) =
viewModelScope.launch(ioDispatcher) {
uiState.withFirstState { state ->
@@ -215,10 +222,15 @@ constructor(
is AppEvent.SetDetectionMethod ->
handleSetDetectionMethod(event.detectionMethod, state.appSettings)
is AppEvent.SaveAllConfigs -> saveAllTunnels(event.tunnels)
}
}
}
private suspend fun saveAllTunnels(tunnels: List<TunnelConf>) {
appDataRepository.tunnels.saveAll(tunnels)
}
private suspend fun handleSetDetectionMethod(
detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
appSettings: AppSettings,
@@ -125,4 +125,6 @@ sealed class AppEvent {
data class SetShowModal(val modalType: AppViewState.ModalType) : AppEvent()
data object ToggleSelectAllTunnels : AppEvent()
data class SaveAllConfigs(val tunnels: List<TunnelConf>) : AppEvent()
}
@@ -0,0 +1,5 @@
package com.zaneschepke.wireguardautotunnel.viewmodel.event
sealed class UiEvent {
data object SortTunnels : UiEvent()
}
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+18
View File
@@ -163,6 +163,24 @@
<string name="learn_more">Zjistit více</string>
<string name="stop">zastavit</string>
<string name="server_ipv4">Překlad názvu hostitele IPv4</string>
<string name="always_on_message2">ujistěte se, že je pro všechny ostatní aplikace vypnutá funkce trvalé připojení VPN, a zkuste to znovu</string>
<string name="use_wildcards">Použít zástupné znaky(wildcards) pro názvy</string>
<string name="multiple">Několik</string>
<string name="enter_config_url">Zadejte adresu URL konfigurace</string>
<string name="search">Hledat</string>
<string name="join_matrix">Připojte se k Matrix komunitě</string>
<string name="join_telegram">Připojte se k Telegram komunitě</string>
<string name="post_down">Po deaktivaci</string>
<string name="invalid_config_error">chyba_neplatné_konfigurace</string>
<string name="error_download_failed">Nepodařilo se stáhnout konfiguraci</string>
<string name="select">Vybrat</string>
<string name="save">Uložit</string>
<string name="dns_resolve_error">Chyba překladu dns</string>
<string name="bio_update_required">Vyžadována aktualizace biometrického zabezpečení</string>
<string name="export_logs">Exportovat uložené protokoly</string>
<string name="add_tunnel">Přidat tunel</string>
<string name="delete_logs">Smazat a vyčistit protokoly</string>
<string name="dropdown">Rozbalovací nabídka</string>
<string name="select_all">Vybrat vše</string>
<string name="share">Sdílet</string>
<string name="trusted_ssid_value_description">Odeslat SSID</string>
+71 -5
View File
@@ -42,7 +42,7 @@
<string name="name">Nombre</string>
<string name="always_on_vpn_support">Permitir VPN siempre-activada</string>
<string name="location_services_not_detected">Servicios de Ubicación No Detectados</string>
<string name="auto_tunneling">Túnel-automático</string>
<string name="auto_tunneling">Túnel automático</string>
<string name="vpn_on">VPN on</string>
<string name="vpn_off">VPN off</string>
<string name="create_import">Crear desde cero</string>
@@ -68,7 +68,7 @@
<string name="error_ssid_exists">SSID existente</string>
<string name="error_root_denied">Shell root denegado</string>
<string name="error_no_file_explorer">Explorador de archivos no instalado</string>
<string name="auto_tunnel_title">Servicio de túnel-automático</string>
<string name="auto_tunnel_title">Servicio de túnel automático</string>
<string name="delete_tunnel">Eliminar túnel</string>
<string name="delete_tunnel_message">¿Estás seguro de que quieres eliminar este túnel?</string>
<string name="yes"></string>
@@ -103,7 +103,7 @@
<string name="always_on_message">Se ha denegado el permiso de conexión VPN. Por favor, compruebe el</string>
<string name="always_on_message2">para asegurarse de que el VPN Siempre encendido esté desactivada para todas las demás aplicaciones e inténtelo de nuevo</string>
<string name="response_packet_magic_header">Encabezado del paquete de respuesta</string>
<string name="junk_packet_maximum_size">Tamaño máximo del paquete basura</string>
<string name="junk_packet_maximum_size">Tamaño máximo del paquete innecesario</string>
<string name="init_packet_junk_size">Tamaño basura del paquete de inicialización</string>
<string name="unsure_how">Si no estás seguro de cómo proceder</string>
<string name="see_the">Ver la</string>
@@ -152,7 +152,7 @@
<string name="light">Claro</string>
<string name="dark">Oscuro</string>
<string name="use_root_shell_for_wifi">Utilizar el shell root para obtener el nombre del wifi</string>
<string name="tunnel_running">Túnel funcionando</string>
<string name="tunnel_running">Túnel conectado</string>
<string name="monitoring_state_changes">Monitorizando cambios de estado</string>
<string name="dynamic">Dinámico</string>
<string name="language">Idioma</string>
@@ -175,7 +175,7 @@
<string name="splt_tunneling">Túnel dividido</string>
<string name="pre_up">Pre up</string>
<string name="post_up">Post up</string>
<string name="pre_down">Pre down</string>
<string name="pre_down">Antes de la desactivación</string>
<string name="post_down">Post down</string>
<string name="quick_actions">Acciones rápidas</string>
<string name="remove_amnezia_compatibility">Eliminar compatibilidad con Amnezia</string>
@@ -185,4 +185,70 @@
<string name="server_ipv4">Resolución de host IPv4</string>
<string name="prefer_ipv4">Preferir conexión IPv4</string>
<string name="multiple">Múltiple</string>
<string name="add_from_url">Añadir desde URL</string>
<string name="save">Guardar</string>
<string name="select">Seleccionar</string>
<string name="join_telegram">Únete a la comunidad de Telegram</string>
<string name="join_matrix">Únete a la comunidad de Matrix</string>
<string name="share">Compartir</string>
<string name="update_download_failed">Fallo al descargar actualización.</string>
<string name="licenses">Licencias</string>
<string name="checking_for_update">Comprobando actualización</string>
<string name="select_all">Seleccionar todos</string>
<string name="permission_required">Permiso requerido</string>
<string name="active">Activo</string>
<string name="copy">Copiar</string>
<string name="bio_not_supported">Datos biométricos no admitidos</string>
<string name="export_tunnels_wireguard">Exportar túneles como WireGuard</string>
<string name="export_tunnels_amnezia">Exportar túneles como Amnezia</string>
<string name="wifi_name_template">Activo: %1$s</string>
<string name="add_tunnel">Añadir túnel</string>
<string name="export_logs">Exportar registros almacenados</string>
<string name="inactive">Inactivo</string>
<string name="search">Buscar</string>
<string name="bio_subtitle">Inicia sesión con tu credencial biométrica</string>
<string name="kernel_name_error">error en el nombre del módulo del kernel</string>
<string name="camera_permission_required">Permiso de cámara requerido</string>
<string name="dns_resolve_error">error de resolución DNS</string>
<string name="error_download_failed">Fallo al descargar configuración</string>
<string name="enter_config_url">Introducir URL de configuración</string>
<string name="bio_update_required">Actualización de seguridad biométrica requerida</string>
<string name="delete_logs">Eliminar y limpiar registros</string>
<string name="app_permission_title">Puente de Control WG Tunnel</string>
<string name="dropdown">Menú desplegable</string>
<string name="info">Info</string>
<string name="delete">Eliminar</string>
<string name="export_failed">Exportación fallida</string>
<string name="app_permission_description">Controlar túneles y funcionalidades de auto-túnel</string>
<string name="tunnel_error_template">Túnel fallido con: %1$s</string>
<string name="invalid_config_error">invalid_config_error</string>
<string name="remote_key_template">Clave: %1$s</string>
<string name="config_error">Error de configuración</string>
<string name="auth_error">error no autorizado</string>
<string name="service_running_error">Error de servicio no en ejecución</string>
<string name="status">Estado</string>
<string name="bio_auth_title">Autenticación biométrica</string>
<string name="version_template">Versión: %1$s</string>
<string name="check_for_update">Comprobar actualización</string>
<string name="update_check_failed">Fallo al comprobar la actualización.</string>
<string name="security_template">Seguridad: %1$s</string>
<string name="flavor_template">Sabor: %1$s</string>
<string name="bio_not_created">Datos biométricos no creados</string>
<string name="tunnel_starting">Iniciando túnel</string>
<string name="enable_remote_app_control">Activar control remoto de la app</string>
<string name="export_success">Éxito al exportar</string>
<string name="download">Descargar</string>
<string name="latest_installed">Ya se está ejecutando la última versión.</string>
<string name="update_available">¡Actualización disponible!</string>
<string name="download_and_install">Descargar e instalar</string>
<string name="install_updated_permission">Esta app necesita permiso para instalar actualizaciónes.</string>
<string name="allow">Permitir</string>
<string name="update_check_unsupported">Comprobación de actualización no permitoda en esta compilación.</string>
<string name="darker">Más oscuro</string>
<string name="amoled">AMOLED</string>
<string name="show_qr">Mostrar QR</string>
<string name="amnezia">Amnezia</string>
<string name="wireguard">WireGuard</string>
<string name="done">Hecho</string>
<string name="nothing_here_yet">¡No hay nada aquí de momento!</string>
</resources>
+228
View File
@@ -0,0 +1,228 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">WG Tunnel</string>
<string name="error_file_extension">See pole .conf või .zip fail</string>
<string name="vpn_channel_name">VPN-i teavituskanal</string>
<string name="turn_off_tunnel">See toiming eeldab, et tunnel pole töös</string>
<string name="prominent_background_location_message">See funktsionaalsus eeldab, et rakendusel on õigus asukoha ja WiFi SSID tuvastamiseks taustal, seda ka siis, kui rakendus on suletud. Lisateavet leiad Privaatsusreeglite lehelt, mille leiad Kasutajatoe vaatest.</string>
<string name="no_tunnels">Ühtegi tunnelit pole veel lisatud!</string>
<string name="tunnels">Tunnelid</string>
<string name="tunnel_mobile_data">Loo tunnel mobiilse andmesidega</string>
<string name="privacy_policy">Vaata privaatsusreegleid</string>
<string name="okay">Sobib</string>
<string name="tunnel_on_ethernet">Loo tunnel kohtvõrgus</string>
<string name="add_tunnels_text">Lisa conf- või zip-failist</string>
<string name="open_file">Ava fail</string>
<string name="add_from_qr">Lisa QR-koodist</string>
<string name="qr_scan">Skaneeri QR-koodi</string>
<string name="tunnel_name">Tunneli nimi</string>
<string name="exclude">Välista</string>
<string name="include">Kaasa</string>
<string name="config_changes_saved">Seadistuse muudatused on salvestatud.</string>
<string name="public_key">Avalik võti</string>
<string name="addresses">Aadressid</string>
<string name="dns_servers">Nimeserverid</string>
<string name="mtu">MTU</string>
<string name="app_permission_description">Halda tunneleid ja tunnelite automaatse käivitamise seadistusi.</string>
<string name="thank_you">Tänud, et kasutad rakendust WG Tunnel!</string>
<string name="allowed_ips">Lubatud IP-aadressid</string>
<string name="endpoint">Otspunkt</string>
<string name="name">Nimi</string>
<string name="always_on_vpn_support">Luba, et VPN on alati sisse lülitatud</string>
<string name="location_services_not_detected">Asukohateenused pole tuvastatavad</string>
<string name="auto_tunneling">Automaatne tunneldus</string>
<string name="vpn_on">VPN on kasutusel</string>
<string name="vpn_off">VPN pole kasutusel</string>
<string name="create_import">Loo nullist</string>
<string name="turn_on_tunnel">Toiming eeldab aktiivse tunneli olemasolu</string>
<string name="licenses">Litsentsid</string>
<string name="update_check_unsupported">Selle rakenduse versiooni puhul pole uuenduste kontrollimine toetatud.</string>
<string name="darker">Tumedam kujundus</string>
<string name="amoled">AMOLED</string>
<string name="show_qr">Näita QR-koodi</string>
<string name="amnezia">Amnezia</string>
<string name="wireguard">WireGuard</string>
<string name="done">Valmis</string>
<string name="download">Laadi alla</string>
<string name="check_for_update">Kontrolli rakenduse uuendusi</string>
<string name="update_check_failed">Uuenduse kontrollimine ei õnnestunud.</string>
<string name="checking_for_update">Kontrollin uuendusi</string>
<string name="latest_installed">Sa juba kasutad viimast versiooni.</string>
<string name="update_download_failed">Uuenduse allalaadimine ei õnnestunud.</string>
<string name="update_available">Uus versioon on saadaval!</string>
<string name="download_and_install">Laadi alla ja paigalda</string>
<string name="permission_required">Õigused on vajalikud</string>
<string name="install_updated_permission">See rakendus vajab uuenduse paigaldamiseks õigusi.</string>
<string name="allow">Luba</string>
<string name="nothing_here_yet">Siin pole veel midagi!</string>
<string name="share">Jaga</string>
<string name="select_all">Vali kõik</string>
<string name="export_success">Eksportimine õnnestus</string>
<string name="inactive">Pole aktiivne</string>
<string name="active">Aktiivne</string>
<string name="status">Olek</string>
<string name="bio_auth_title">Biomeetriline autentimine</string>
<string name="bio_subtitle">Logi sisse kasutades biomeetrilist autentimist</string>
<string name="bio_not_supported">Biomeetriline tuvastamine pole toetatud</string>
<string name="bio_not_created">Biomeetriline tuvastamine pole seadistatud</string>
<string name="bio_update_required">Vajalik on biomeetrilise tuvastamise turvauuendus</string>
<string name="tunnel_starting">Tunnel käivitub</string>
<string name="enable_remote_app_control">Luba rakenduse kaugjuhtimine</string>
<string name="add_from_url">Lisa võrguaadressilt</string>
<string name="enter_config_url">Sisesta seadistuse võrguaadress</string>
<string name="error_download_failed">Seadistuse allalaadimine ei õnnestunud</string>
<string name="save">Salvesta</string>
<string name="search">Otsi</string>
<string name="select">Vali</string>
<string name="join_telegram">Liitu kogukonnaga Telegramis</string>
<string name="join_matrix">Liitu kogukonnaga Matrixis</string>
<string name="add_tunnel">Lisa tunnel</string>
<string name="export_logs">Ekspordi salvestatud logid</string>
<string name="delete_logs">Kustuta ja eemalda logid</string>
<string name="copy">Kopeeri</string>
<string name="info">Teave</string>
<string name="export_tunnels_amnezia">Ekspordi tunnelid Amnezia jaoks</string>
<string name="export_tunnels_wireguard">Ekspordi tunnelid WireGuardi jaoks</string>
<string name="advanced_settings">Täiendavad seadistused</string>
<string name="show_scripts">Näita skripte</string>
<string name="pre_up">Käivituseelne</string>
<string name="post_up">Käivitusjärgne</string>
<string name="pre_down">Sulgemiseelne</string>
<string name="post_down">Sulgemijärgne</string>
<string name="hide_scripts">Peida skriptid</string>
<string name="enable_amnezia_compatibility">Lisa ühilduvus Amnezia teenustega</string>
<string name="remove_amnezia_compatibility">Eemalda ühilduvus Amnezia teenustega</string>
<string name="show_amnezia_properties">Näita Amnezia seadistusi</string>
<string name="hide_amnezia_properties">Peida Amnezia seadistused</string>
<string name="donate">Toeta projekti rahaliselt</string>
<string name="local_logging">Kohalik logimine</string>
<string name="enable_local_logging">Logi andmed nutiseadmes</string>
<string name="add_from_clipboard">Lisa lõikelaualt</string>
<string name="appearance">Välimus</string>
<string name="notifications">Teavitused</string>
<string name="automatic">Automaatne</string>
<string name="light">Hele kujundus</string>
<string name="dark">Tume kujundus</string>
<string name="dynamic">Dünaamiline kujundus</string>
<string name="language">Keel</string>
<string name="display_theme">Kujundus</string>
<string name="interface_">Võrguliides</string>
<string name="private_key">Privaatvõti</string>
<string name="copy_public_key">Kopeeri avalik võti</string>
<string name="base64_key">base64-kodeeringus võti</string>
<string name="comma_separated_list">komadega eraldatud loend</string>
<string name="listen_port">Kuulatav port</string>
<string name="random">(juhuslik)</string>
<string name="optional">(valikuline)</string>
<string name="preshared_key">Eeljagatud võti</string>
<string name="seconds">sekundit</string>
<string name="persistent_keepalive">Pidev elumärksõnum</string>
<string name="cancel">Katkesta</string>
<string name="error_authentication_failed">Autentimine ei õnnestunud</string>
<string name="exclude_lan">Välista kohtvõrgud</string>
<string name="include_lan">Kaasa kohtvõrgud</string>
<string name="dns_resolve_error">nimelahenduse viga</string>
<string name="peer">Partner</string>
<string name="add_peer">Lisa partner</string>
<string name="default_ping_ip">(valikuline, vaikimisi partneri otspunkt)</string>
<string name="rotate_keys">Vaheta võtmeid</string>
<string name="delete_tunnel">Kustuta tunnel</string>
<string name="delete_tunnel_message">Kas sa oled kindel, et soovid selle tunneli kustutada?</string>
<string name="yes">Jah</string>
<string name="all">kõik</string>
<string name="no_email_detected">E-posti rakendust ei õnnestu tuvastada</string>
<string name="no_browser_detected">Veebibrauserit ei õnnestu tuvastada</string>
<string name="open_issue">Alusta veateate koostamist</string>
<string name="read_logs">Loe logisid</string>
<string name="auto">(automaatne)</string>
<string name="incorrect_pin">PIN-kood pole õige</string>
<string name="pin_created">PIN-koodi loomine õnnestus</string>
<string name="enter_pin">Sisesta oma PIN-kood</string>
<string name="create_pin">Loo PIN-kood</string>
<string name="edit_tunnel">Muuda tunnelit</string>
<string name="version">Versioon</string>
<string name="settings">Seadistused</string>
<string name="support">Kasutajatugi</string>
<string name="unknown_error">Tekkis tundmatu viga</string>
<string name="tunnel_on_wifi">Loo tunnel ebausaldusväärses WiFi võrgus</string>
<string name="email_subject">WG Tunneli kasutajatugi</string>
<string name="email_chooser">Saada e-kiri…</string>
<string name="docs_description">Loe dokumentatsiooni</string>
<string name="email_description">Saada mulle e-kiri</string>
<string name="use_kernel">Kasuta kernelimoodulit</string>
<string name="error_ssid_exists">SSID on juba olemas</string>
<string name="error_root_denied">Juurkasutaja õigustes kest on keelatud</string>
<string name="error_no_file_explorer">Failihaldurit pole paigaldatud</string>
<string name="set_primary_tunnel">Määra põhiliseks tunneliks</string>
<string name="skip">Jäta vahele</string>
<string name="export_failed">Eksportimine ei õnnestunud</string>
<string name="tunnel_error_template">Viga tunneli töös: %1$s</string>
<string name="wifi_name_template">Aktiivne: %1$s</string>
<string name="remote_key_template">Võti: %1$s</string>
<string name="version_template">Versioon: %1$s</string>
<string name="security_template">Turvalisus: %1$s</string>
<string name="flavor_template">Levitusviis: %1$s</string>
<string name="config_error">seadistusviga</string>
<string name="invalid_config_error">vigane_seadistus_viga</string>
<string name="kernel_name_error">viga kerneli mooduli nimes</string>
<string name="auth_error">viga autentimisel</string>
<string name="service_running_error">viga, kus teenus ei toimi</string>
<string name="unsure_how">kui sa ei tea, mida järgmiseks teha</string>
<string name="see_the">Vaata</string>
<string name="getting_started_guide">esimeste toimingute juhendit</string>
<string name="restart_at_boot">Käivita alglaadimisel uuesti</string>
<string name="vpn_denied_dialog_title">Õigused on puudu</string>
<string name="set_custom_ping_internal">Pingi välp (sekundites)</string>
<string name="optional_default">"valikuline, vaikimisi: "</string>
<string name="never">mitte kunagi</string>
<string name="sec">sek</string>
<string name="handshake">kätlus</string>
<string name="logs">Logid</string>
<string name="kill_switch">Kiirpeatamine</string>
<string name="trusted_wifi_names">Usaldusväärsete WiFi-võrkude nimed</string>
<string name="add_wifi_name">Lisa WiFi võrgunimi</string>
<string name="primary_tunnel">Põhiline tunnel</string>
<string name="app_settings">rakenduse seadistused</string>
<string name="background_location_message2">tagamaks, et need õigused on lubatud</string>
<string name="root_accepted">Juurkasutaja kest on lubatud</string>
<string name="set_custom_ping_ip">Sisesta muu ip-aadress</string>
<string name="learn_more">Lisateave</string>
<string name="monitoring_state_changes">Jälgin oleku muudatusi</string>
<string name="tunnel_running">Tunnel töötab</string>
<string name="tunnel_specific_settings">Tunnelikohased seadistused</string>
<string name="kernel_not_supported">Kernel pole toetatud</string>
<string name="start_auto">Käivita automaatne tunneldus</string>
<string name="stop_auto">Peata automaatne tunneldus</string>
<string name="quick_actions">Kiirtoimingud</string>
<string name="tunnel_control">Tunneli juhtimine</string>
<string name="auto_tunnel">Automaatne tunneldus</string>
<string name="delete">Kustuta</string>
<string name="camera_permission_required">Vajalik on õigus kasutada kaamerat</string>
<string name="dropdown">Rippmenüü</string>
<string name="splt_tunneling">Jagatud tunneldus</string>
<string name="stop">peata</string>
<string name="stop_on_no_internet">Peata internetiühenduse puudumisel</string>
<string name="stop_on_internet_loss">Peata tunnel internetiühenduse kadumisel</string>
<string name="enable_app_lock">Kasuta rakenduse lukustust</string>
<string name="restart_on_ping">Pingimise mittetoimimisel käivita uuesti (beeta)</string>
<string name="mobile_data_tunnel">Määra mobiilse andmeside tunneliks</string>
<string name="use_tunnel_on_wifi_name">Kasuta tunnelit WiFi nime puhul</string>
<string name="kernel">Tuum/Kernel</string>
<string name="error_file_format">Vigane tunneli seadistuste vorming</string>
<string name="mobile_tunnel">Mobiilside andmetunnel</string>
<string name="launch_app_settings">Käivita rakenduse seadistused</string>
<string name="use_wildcards">Kasuta nimedes metamärke</string>
<string name="wildcards_active">Metamärgid on kasutusel</string>
<string name="prefer_ipv4">Eelista IPv4 ühendust</string>
<string name="server_ipv4">IPv4 hostinime nimelahendus</string>
<string name="junk_packet_count">Rämpspakettide arv</string>
<string name="junk_packet_minimum_size">Rämpspaketi miinimumsuurus</string>
<string name="junk_packet_maximum_size">Rämpspaketi maksimumsuurus</string>
<string name="init_packet_junk_size">Alustava paketi rämpsuosa suurus</string>
<string name="response_packet_junk_size">Vastuspaketi rämpsuosa suurus</string>
<string name="init_packet_magic_header">Alustava paketi päise kohandatud osa</string>
<string name="response_packet_magic_header">Vastuspaketi päise kohandatud osa</string>
<string name="transport_packet_magic_header">Transpordipaketi päise kohandatud osa</string>
<string name="underload_packet_magic_header">Väikese koormusega paketi päise kohandatud osa</string>
<string name="vpn_settings">VPN-i süsteemsed seadistused</string>
</resources>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_permission_title">پل کنترل تونل وایرگارد</string>
<string name="privacy_policy">مشاهده سیاست حفظ حریم خصوصی</string>
<string name="vpn_channel_name">کانال اطلاع رسانی VPN</string>
<string name="error_file_extension">فایل .conf یا .zip نیست.</string>
<string name="turn_off_tunnel">اقدام نیاز به تونل زنی دارد</string>
<string name="no_tunnels">هنوز تونلی اضافه نشده!</string>
<string name="tunnels">تونل‌ها</string>
<string name="tunnel_mobile_data">تونل روی داده تلفن همراه</string>
<string name="okay">باشه</string>
<string name="prominent_background_location_title">افشای موقعیت مکانی پس‌زمینه</string>
<string name="thank_you">از استفاده شما از تونل WG متشکریم!</string>
<string name="trusted_ssid_value_description">ارسال SSID</string>
<string name="add_tunnels_text">از فایل یا زیپ اضافه کنید</string>
<string name="app_permission_description">تونل‌های کنترل و ویژگی‌های تونل خودکار.</string>
<string name="tunnel_on_ethernet">تونل روی اترنت</string>
<string name="prominent_background_location_message">این ویژگی برای فعال کردن نظارت بر SSID وای‌فای، حتی در زمان بسته بودن برنامه، به مجوز موقعیت مکانی در پس‌زمینه نیاز دارد. برای جزئیات بیشتر، لطفاً به سیاست حفظ حریم خصوصی که در صفحه پشتیبانی لینک شده است، مراجعه کنید.</string>
<string name="app_name">WG Tunnel</string>
</resources>
+12
View File
@@ -115,4 +115,16 @@
<string name="sec">sek</string>
<string name="read_logs">Lue lokitiedot</string>
<string name="mobile_tunnel">Mobiilidatatunneli</string>
<string name="restart_at_boot">Käynnistä laitteen käynnistyksen yhteydessä</string>
<string name="always_on_message2">varmistaaksesi, että Aina päällä oleva VPN on kytketty pois päältä kaikkien muiden sovellusten osalta ja yritä uudelleen</string>
<string name="background_location_message">Tätä toimintoa varten tulee sijainnin käyttölupa olla aina sallittuna ja/tai tarkka sijainti käytössä. Katso</string>
<string name="app_settings">sovelluksen asetukset</string>
<string name="root_accepted">Root hyväksytty</string>
<string name="set_custom_ping_ip">Määrittele pingin ip-osoite</string>
<string name="set_custom_ping_internal">Pingin aikaväli (sek)</string>
<string name="show_amnezia_properties">Näytä Amnezia-asetukset</string>
<string name="launch_app_settings">Käynnistä sovelluksen asetukset</string>
<string name="use_wildcards">Käytä jokerimerkkiä nimissä</string>
<string name="wildcards_active">Jokerimerkit aktivoitu</string>
<string name="tunnel_running">Tunneli käytössä</string>
</resources>
+3
View File
@@ -187,4 +187,7 @@
<string name="copy">Copier</string>
<string name="info">Informations</string>
<string name="prefer_ipv4">Préférer une connexion IPv4</string>
<string name="allow">Autoriser</string>
<string name="app_permission_title">Pont de contrôle du tunnel WG</string>
<string name="app_permission_description">Contrôler les tunnels et les fonctions automatiques des tunnels.</string>
</resources>
+1
View File
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">WG Tunnel</string>
<string name="app_permission_description">Alagutak és automatikus alagút funkciók vezérlése.</string>
</resources>
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="app_name">WG Tunnel</string>
<string name="app_permission_description">Kontroller tunneler og auto-tunnel egenskaper.</string>
<string name="vpn_channel_name">VPN varslingskanal</string>
</resources>
-163
View File
@@ -1,163 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tunnel_name">Nome do Túnel</string>
<string name="exclude">Excluir</string>
<string name="include">Incluir</string>
<string name="config_changes_saved">Mudanças nas configurações gravadas.</string>
<string name="public_key">Chave pública</string>
<string name="app_name">WG Tunnel</string>
<string name="vpn_channel_name">Canal de notificações VPN</string>
<string name="error_file_extension">O ficheiro não é .conf ou .zip</string>
<string name="turn_off_tunnel">Esta ação só é possível com o túnel inativo</string>
<string name="no_tunnels">Nenhum túnel foi adicionado!</string>
<string name="tunnels">Túneis</string>
<string name="tunnel_mobile_data">Túnel em dados móveis</string>
<string name="privacy_policy">Ver a Política de Privacidade</string>
<string name="okay">OK</string>
<string name="tunnel_on_ethernet">Túnel na ethernet</string>
<string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para ativar o monitoramento do SSID da rede Wi-Fi mesmo quando a aplicação está fechado. Para mais pormenores, por favor veja a Política de Privacidade no ecrã de Suporte.</string>
<string name="prominent_background_location_title">Revelar a localização em segundo plano</string>
<string name="thank_you">Obrigado por usar o WG Tunnel!</string>
<string name="trusted_ssid_value_description">Envie o SSID</string>
<string name="add_tunnels_text">Adicionar a partir de ficheiro ou zip</string>
<string name="open_file">Abrir Ficheiro</string>
<string name="add_from_qr">Adicionar a partir de código QR</string>
<string name="qr_scan">Escanear o código QR</string>
<string name="addresses">Endereços</string>
<string name="dns_servers">Servidores DNS</string>
<string name="mtu">MTU</string>
<string name="peer">Par</string>
<string name="allowed_ips">IPs Permitidos</string>
<string name="name">Nome</string>
<string name="always_on_vpn_support">Permitir VPN sempre ligada</string>
<string name="location_services_not_detected">Serviço de localização não foi detetado</string>
<string name="auto_tunneling">Auto-túnel</string>
<string name="vpn_on">VPN ligada</string>
<string name="vpn_off">VPN desligada</string>
<string name="create_import">Criar do zero</string>
<string name="turn_on_tunnel">Esta ação precisa um túnel ativo</string>
<string name="add_peer">Adicionar par</string>
<string name="interface_">Interface</string>
<string name="rotate_keys">Revezar chaves</string>
<string name="private_key">Chave privada</string>
<string name="copy_public_key">Copiar chave pública</string>
<string name="base64_key">Chave base64</string>
<string name="comma_separated_list">Lista separada por vírgulas</string>
<string name="listen_port">Porta de escuta</string>
<string name="random">(aleatório)</string>
<string name="optional">(opcional)</string>
<string name="preshared_key">Chave pré-partilhada</string>
<string name="seconds">segundos</string>
<string name="persistent_keepalive">Manter a conexão persistente (keepalive)</string>
<string name="cancel">Cancelar</string>
<string name="error_authentication_failed">Autenticação falhou</string>
<string name="error_authorization_failed">Autorização falhou</string>
<string name="enabled_app_shortcuts">Ativar atalhos de aplicações</string>
<string name="unknown_error">Ocorreu um erro desconhecido</string>
<string name="tunnel_on_wifi">Túnel em Wi-Fi não confiável</string>
<string name="email_subject">Apoio para o WG Tunnel</string>
<string name="email_chooser">Enviar um email…</string>
<string name="docs_description">Ler a documentação</string>
<string name="email_description">Me envie um email</string>
<string name="use_kernel">Usar o módulo do kernel</string>
<string name="error_ssid_exists">SSID já existe</string>
<string name="error_root_denied">Shell Root negado</string>
<string name="error_no_file_explorer">Nenhum explorador de ficheiros instalado</string>
<string name="location_services_missing_message">A aplicação não detetou o serviço de localização ativado no seu dispositivo. Dependendo do dispositivo, isto pode causar que a função de Wi-Fi não confiável falhe em ler o nome do Wi-Fi. Quer continuar mesmo assim?</string>
<string name="auto_tunnel_title">Serviço de Auto-túnel</string>
<string name="delete_tunnel">Apagar túnel</string>
<string name="delete_tunnel_message">Tem certeza que quer apagar este túnel?</string>
<string name="yes">Sim</string>
<string name="all">todos</string>
<string name="no_email_detected">Nenhuma aplicação de email detetado</string>
<string name="no_browser_detected">Nenhum navegador detetado</string>
<string name="open_issue">Abrir um problema</string>
<string name="read_logs">Ler os registos</string>
<string name="auto">(automático)</string>
<string name="incorrect_pin">O Pin está errado</string>
<string name="pin_created">Pin criado com sucesso</string>
<string name="enter_pin">Digite o seu pin</string>
<string name="create_pin">Criar um pin</string>
<string name="enable_app_lock">Ligar bloqueio de aplicação</string>
<string name="restart_on_ping">Reiniciar em falha de ping (beta)</string>
<string name="mobile_data_tunnel">Selecionar como túnel em dados móveis</string>
<string name="set_primary_tunnel">Selecionar como túnel principal</string>
<string name="use_tunnel_on_wifi_name">Usar túnel em wifi com nome</string>
<string name="edit_tunnel">Editar túnel</string>
<string name="version">Versão</string>
<string name="settings">Configurações</string>
<string name="support">Suporte</string>
<string name="kernel">Kernel</string>
<string name="junk_packet_count">Quantidade de pacotes-lixo</string>
<string name="junk_packet_minimum_size">Tamanho mínimo de pacote-lixo</string>
<string name="junk_packet_maximum_size">Tamanho máximo de pacote-lixo</string>
<string name="init_packet_junk_size">Tamanho de pacote-lixo inicial</string>
<string name="response_packet_junk_size">Tamanho de resposta de pacote-lixo</string>
<string name="unsure_how">se não tiver certeza em como continuar</string>
<string name="see_the">Veja o</string>
<string name="getting_started_guide">guia de início rápido</string>
<string name="error_file_format">Formato de configuração inválido</string>
<string name="restart_at_boot">Ativar na inicialização</string>
<string name="vpn_denied_dialog_title">Permissão negada</string>
<string name="vpn_settings">Configurações do sistema VPN</string>
<string name="always_on_message">A permissão de conexão VPN foi negada. Por favor, verifique</string>
<string name="always_on_message2">para ter certeza que VPN Sempre-ligada é desligada para todas as outras aplicações e tente novamente</string>
<string name="background_location_message">Permitir que toda a permissão de localização do tempo e/ou localização precisa é necessária para este recurso. Por favor, veja</string>
<string name="app_settings">configurações da app</string>
<string name="root_accepted">Shell root aceito</string>
<string name="set_custom_ping_ip">Definir ip ping personalizado</string>
<string name="default_ping_ip">(opcional, padrão para pares)</string>
<string name="set_custom_ping_internal">Intervalo de Ping (seg)</string>
<string name="optional_default">"opcional, padrão: "</string>
<string name="show_amnezia_properties">Mostrar propriedades de Amnezia</string>
<string name="never">nunca</string>
<string name="sec">seg</string>
<string name="handshake">handshake</string>
<string name="appearance">Aparência</string>
<string name="notifications">Notificações</string>
<string name="automatic">Automático</string>
<string name="light">Claro</string>
<string name="dark">Escuro</string>
<string name="dynamic">Dinâmico</string>
<string name="language">Idioma</string>
<string name="display_theme">Tema</string>
<string name="trusted_wifi_names">Nomes de Wi-Fi confiáveis</string>
<string name="add_wifi_name">Adicionar nome Wi-Fi</string>
<string name="mobile_tunnel">Túnel com dados móveis</string>
<string name="skip">Pular</string>
<string name="use_wildcards">Usar nomes coringas</string>
<string name="learn_more">Saber mais</string>
<string name="wildcards_active">Wildcards ativos</string>
<string name="wifi_name_via_shell">Nome do Wi-Fi por shell</string>
<string name="use_root_shell_for_wifi">Obter o nome do Wi-Fi através do shell root</string>
<string name="kernel_not_supported">Kernel não suportado</string>
<string name="start_auto">Iniciar túnel automático</string>
<string name="stop_auto">Pausar túnel automático</string>
<string name="tunnel_running">Túnel em execução</string>
<string name="monitoring_state_changes">Monitorar estado de alterações</string>
<string name="donate">Contribua com projeto</string>
<string name="local_logging">Registo local</string>
<string name="enable_local_logging">Ativar registo local</string>
<string name="add_from_clipboard">Adicionar da área de transferência</string>
<string name="stop_on_no_internet">Interromper quando não há internet</string>
<string name="stop_on_internet_loss">Interrompa o túnel quando a internet não estiver disponível</string>
<string name="ethernet_tunnel">Túnel ethernet</string>
<string name="set_ethernet_tunnel">Definir como túnel ethernet</string>
<string name="native_kill_switch">Interruptor de desligamento padrão</string>
<string name="vpn_kill_switch">Interruptor de desligamento VPN</string>
<string name="kill_switch_options">Opções do interruptor de desligamento</string>
<string name="allow_lan_traffic">Permitir tráfego LAN</string>
<string name="bypass_lan_for_kill_switch">Ignorar LAN no interruptor de desligamento</string>
<string name="stop">pausar</string>
<string name="splt_tunneling">Tunelamento dividido</string>
<string name="tunnel_specific_settings">Configurações específicas no túnel</string>
<string name="show_scripts">Mostrar scripts</string>
<string name="quick_actions">Ações rápidas</string>
<string name="advanced_settings">Configurações avançadas</string>
<string name="hide_amnezia_properties">Ocultar propriedades Amnezia</string>
<string name="hide_scripts">Ocultar scripts</string>
<string name="enable_amnezia_compatibility">Ativar compatibilidade Amnezia</string>
<string name="remove_amnezia_compatibility">Remover compatibilidade Amnezia</string>
<string name="exclude_lan">Excluir LAN</string>
<string name="include_lan">Incluir LAN</string>
</resources>
+1 -1
View File
@@ -179,7 +179,7 @@
<string name="advanced_settings">Дополнительные настройки</string>
<string name="enable_amnezia_compatibility">Включить совместимость с Amnezia</string>
<string name="include_lan">Включить LAN</string>
<string name="auto_tunnel">Авто-туннелирование</string>
<string name="auto_tunnel">Автотуннелирование</string>
<string name="tunnel_control">Управление туннелями</string>
<string name="kill_switch_off">Без экстренного отключения в доверенных</string>
<string name="prefer_ipv4">Предпочитать соединение IPv4</string>
+141 -137
View File
@@ -2,250 +2,254 @@
<resources>
<string name="peer">پیر</string>
<string name="add_from_qr">کیو آر کوڈ سے شامل کریں</string>
<string name="always_on_vpn_support">ہمیشہ آن VPN کی اجازت دیں۔</string>
<string name="add_peer">ساتھی شامل کریں۔</string>
<string name="turn_off_tunnel">کارروائی کے لیے سرنگ بند کی ضرورت ہے۔</string>
<string name="rotate_keys">چابیاں گھمائیں۔</string>
<string name="copy_public_key">عوامی کلید کو کاپی کریں۔</string>
<string name="vpn_channel_name">VPN نوٹیفکیشن چینل</string>
<string name="always_on_vpn_support">ہمیشہ آن وی پی این کی اجازت دیں</string>
<string name="add_peer">پیئر شامل کریں</string>
<string name="turn_off_tunnel">کارروائی کے لیے ٹنل بند کی ضرورت ہے</string>
<string name="rotate_keys">کلید گھمائیں</string>
<string name="copy_public_key">عوامی کلید نقل کریں</string>
<string name="vpn_channel_name">وی پی این نوٹیفکیشن چینل</string>
<string name="optional">(اختیاری)</string>
<string name="comma_separated_list">کوما سے الگ کردہ فہرست</string>
<string name="location_services_missing_message">ایپ آپ کے آلے پر فعال مقام کی خدمات کا پتہ نہیں لگا رہی ہے۔ ڈیوائس پر منحصر ہے، یہ غیر بھروسہ مند وائی فائی فیچر کے وائی فائی کا نام پڑھنے میں ناکام ہونے کا سبب بن سکتا ہے۔ کیا آپ بہرحال جاری رکھنا چاہیں گے؟</string>
<string name="prominent_background_location_title">پس منظر کی جگہ کا انکشاف</string>
<string name="location_services_missing_message">ایپ آپ کے آلے پر فعال مقام کی سروسز کا پتہ نہیں لگا رہی۔ ڈیوائس پر منحصر ہے، یہ غیر بھروسہ مند وائی فائی فیچر کے وائی فائی کا نام پڑھنے میں ناکام ہونے کا سبب بن سکتا ہے۔ کیا آپ بہرحال جاری رکھنا چاہیں گے؟</string>
<string name="prominent_background_location_title">پس منظر لوکیشن کا انکشاف</string>
<string name="public_key">عوامی کلید</string>
<string name="app_name">ڈبلیو جی ٹنل</string>
<string name="error_file_extension">فائل conf یا zip نہیں ہے۔</string>
<string name="no_tunnels">ابھی تک کوئی سرنگیں شامل نہیں کی گئیں!</string>
<string name="no_tunnels">ابھی تک کوئی ٹنلز شامل نہیں کی گئیں!</string>
<string name="tunnels">ٹنلز</string>
<string name="tunnel_mobile_data">موبائل ڈیٹا پر ٹنل</string>
<string name="privacy_policy">رازداری کی پالیسی دیکھیں</string>
<string name="okay">ٹھیک ہے۔</string>
<string name="tunnel_on_ethernet">ایتھرنیٹ پر ٹنل</string>
<string name="tunnel_on_ethernet">ای تھرنیٹ پر ٹنل</string>
<string name="thank_you">ڈبلیو جی ٹنل استعمال کرنے کا شکریہ!</string>
<string name="trusted_ssid_value_description">SSID جمع کروائیں۔</string>
<string name="open_file">فائل کھولیں۔</string>
<string name="trusted_ssid_value_description">ایس ایس آی ڈی جمع کروائیں</string>
<string name="open_file">فائل کھولیں</string>
<string name="qr_scan">کیو آر اسکین</string>
<string name="tunnel_name">ٹنل کا نام</string>
<string name="exclude">خارج کرنا</string>
<string name="include">شامل کریں۔</string>
<string name="exclude">خارج</string>
<string name="include">شامل</string>
<string name="addresses">پتے</string>
<string name="dns_servers">DNS سرورز</string>
<string name="dns_servers">ڈی این ایس سرورز</string>
<string name="mtu">ایم ٹی یو</string>
<string name="allowed_ips">اجازت یافتہ IPs</string>
<string name="allowed_ips">اجازت یافتہ آئی پی</string>
<string name="endpoint">اختتامی نقطہ</string>
<string name="name">نام</string>
<string name="location_services_not_detected">مقام کی خدمات کا پتہ نہیں چلا</string>
<string name="vpn_on">VPN آن</string>
<string name="vpn_off">VPN آف ہے۔</string>
<string name="location_services_not_detected">مقام کی سروسز کا پتہ نہیں چلا</string>
<string name="vpn_on">وی پی این آن</string>
<string name="vpn_off">وی پی این آف</string>
<string name="turn_on_tunnel">کارروائی کے لیے فعال ٹنل کی ضرورت ہے</string>
<string name="interface_">انٹرفیس</string>
<string name="private_key">نجی کلید</string>
<string name="base64_key">بیس 64 کلید</string>
<string name="base64_key">بیس64 کلید</string>
<string name="listen_port">لسن پورٹ</string>
<string name="random">(بے ترتیب)</string>
<string name="preshared_key">پہلے سے مشترکہ کلید</string>
<string name="cancel">منسوخ کریں۔</string>
<string name="error_authentication_failed">تصدیق ناکام ہوگئی</string>
<string name="cancel">منسوخ</string>
<string name="error_authentication_failed">تصدیق ناکام</string>
<string name="error_authorization_failed">اجازت دینے میں ناکام</string>
<string name="enabled_app_shortcuts">ایپ شارٹ کٹس کو فعال کریں۔</string>
<string name="enabled_app_shortcuts">ایپ شارٹ کٹس کو فعال کریں</string>
<string name="tunnel_on_wifi">ناقابل اعتماد وائی فائی پر ٹنل</string>
<string name="email_subject">ڈبلیو جی ٹنل سپورٹ</string>
<string name="docs_description">دستاویزات پڑھیں</string>
<string name="email_description">مجھے ایک ای میل بھیجیں۔</string>
<string name="use_kernel">کرنل ماڈیول استعمال کریں۔</string>
<string name="error_ssid_exists">SSID پہلے سے موجود ہے۔</string>
<string name="error_root_denied">روٹ شیل سے انکار کر دیا گیا۔</string>
<string name="error_no_file_explorer">کوئی فائل ایکسپلورر انسٹال نہیں ہے۔</string>
<string name="email_description">مجھے ایک ای میل بھیجیں</string>
<string name="use_kernel">کرنل ماڈیول استعمال کریں</string>
<string name="error_ssid_exists">ایس ایس آی ڈی پہلے سے موجود ہے</string>
<string name="error_root_denied">روٹ شیل سے انکار کر دیا گیا</string>
<string name="error_no_file_explorer">کوئی فائل ایکسپلورر انسٹال نہیں ہے</string>
<string name="auto_tunnel_title">خودکار ٹنل سروس</string>
<string name="delete_tunnel">ٹنل کو حذف کریں</string>
<string name="delete_tunnel">ٹنل حذف کریں</string>
<string name="delete_tunnel_message">کیا آپ واقعی اس ٹنل کو حذف کرنا چاہیں گے؟</string>
<string name="yes">جی ہاں</string>
<string name="yes">ہاں</string>
<string name="all">تمام</string>
<string name="no_email_detected">کوئی ای میل ایپ نہیں ملی</string>
<string name="no_browser_detected">کوئی براؤزر نہیں ملا</string>
<string name="open_issue">ایک مسئلہ کھولیں۔</string>
<string name="read_logs">نوشتہ جات پڑھیں</string>
<string name="open_issue">ایک مسئلہ کھولیں</string>
<string name="read_logs">لاگز پڑھیں</string>
<string name="auto">(خودکار)</string>
<string name="incorrect_pin">پن غلط ہے۔</string>
<string name="pin_created">پن کامیابی کے ساتھ بن گیا۔</string>
<string name="incorrect_pin">پن غلط ہے</string>
<string name="pin_created">پن کامیابی کے ساتھ بن گیا</string>
<string name="create_pin">پن بنائیں</string>
<string name="restart_on_ping">پنگ فیل پر دوبارہ شروع کریں (بی ٹا)</string>
<string name="set_primary_tunnel">بنیادی ٹنل کے طور پر سیٹ کریں</string>
<string name="version">ورژن</string>
<string name="junk_packet_count">ردی کے پیکٹ کی گنتی</string>
<string name="init_packet_junk_size">Init پیکٹ ردی کا سائز</string>
<string name="init_packet_magic_header">Init پیکٹ میجک ہیڈر</string>
<string name="junk_packet_count">جنک پیکٹ کی تعداد</string>
<string name="init_packet_junk_size">اِنِٹ پیکٹ جنک سائز</string>
<string name="init_packet_magic_header">اِنِٹ پیکٹ میجک ہیڈر</string>
<string name="transport_packet_magic_header">ٹرانسپورٹ پیکٹ میجک ہیڈر</string>
<string name="see_the">دیکھیں</string>
<string name="error_file_format">غلط ٹنل کنفیگریشن فارمیٹ</string>
<string name="vpn_denied_dialog_title">اجازت نامنظور</string>
<string name="vpn_settings">VPN سسٹم کی ترتیبات</string>
<string name="always_on_message">VPN کنکشن کی اجازت مسترد کر دی گئی ہے۔ براہ کرم چیک کریں۔</string>
<string name="vpn_settings">وی پی این سسٹم کی ترتیبات</string>
<string name="always_on_message">وی پی این کنکشن کی اجازت مسترد کر دی گئی ہے۔ براہ کرم چیک کریں</string>
<string name="app_settings">ایپ کی ترتیبات</string>
<string name="root_accepted">روٹ شیل کو قبول کر لیا گیا۔</string>
<string name="set_custom_ping_ip">اپنی مرضی کے مطابق پنگ آئی پی سیٹ کریں۔</string>
<string name="default_ping_ip">(اختیاری، ساتھیوں کے لیے ڈیفالٹس)</string>
<string name="show_amnezia_properties">Amnezia کی خصوصیات دکھائیں۔</string>
<string name="root_accepted">روٹ شیل قبول کر لیا گیا</string>
<string name="set_custom_ping_ip">اپنی مرضی کے مطابق پنگ آئی پی سیٹ کریں</string>
<string name="default_ping_ip">(اختیاری، پیئرز کے لیے طہ شدہ)</string>
<string name="show_amnezia_properties">ایمنیزیا خصوصیات دکھائیں</string>
<string name="sec">سیکنڈ</string>
<string name="appearance">ظاہری شکل</string>
<string name="notifications">اطلاعات</string>
<string name="automatic">خودکار</string>
<string name="dark">اندھیرا</string>
<string name="dark">تاریک</string>
<string name="dynamic">متحرک</string>
<string name="language">زبان</string>
<string name="display_theme">ڈسپلے تھیم</string>
<string name="trusted_wifi_names">قابل اعتماد وائی فائی نام</string>
<string name="primary_tunnel">بنیادی ٹنل</string>
<string name="mobile_tunnel">موبائل ڈیٹا ٹنل</string>
<string name="skip">چھوڑیں۔</string>
<string name="skip">نظر انداز</string>
<string name="learn_more">مزید جانیں</string>
<string name="kernel_not_supported">کرنل تعاون یافتہ نہیں ہے۔</string>
<string name="start_auto">خودکار ٹنل شروع کریں</string>
<string name="donate">پروجیکٹ کے لیے عطیہ کریں۔</string>
<string name="kernel_not_supported">کرنل سپورٹڈ نہیں</string>
<string name="start_auto">خودکار ٹنل چلائیں</string>
<string name="donate">پروجیکٹ کے لیے عطیہ کریں</string>
<string name="local_logging">مقامی لاگنگ</string>
<string name="enable_local_logging">مقامی لاگنگ کو فعال کریں۔</string>
<string name="stop_on_no_internet">بغیر انٹرنیٹ پر رکیں۔</string>
<string name="stop_on_internet_loss">انٹرنیٹ بند ہونے پر ٹنل روکیں</string>
<string name="vpn_kill_switch">VPN کِل سوئچ</string>
<string name="kill_switch_options">کِل سوئچ کے اختیارات</string>
<string name="allow_lan_traffic">LAN ٹریفک کی اجازت دیں۔</string>
<string name="enable_local_logging">مقامی لاگنگ فعال کریں</string>
<string name="stop_on_no_internet">بغیر انٹرنیٹ پر روکیں</string>
<string name="stop_on_internet_loss">انٹرنیٹ لاس پر ٹنل روکیں</string>
<string name="vpn_kill_switch">وی پی این کِل سوئچ</string>
<string name="kill_switch_options">کِل سوئچ اختیارات</string>
<string name="allow_lan_traffic">لین ٹریفک کی اجازت دیں</string>
<string name="splt_tunneling">سپلٹ ٹنلنگ</string>
<string name="tunnel_specific_settings">ٹنل کی مخصوص ترتیبات</string>
<string name="show_scripts">اسکرپٹ دکھائیں۔</string>
<string name="show_scripts">اسکرپٹس دکھائیں</string>
<string name="pre_up">پری اپ</string>
<string name="post_up">پوسٹ اپ</string>
<string name="pre_down">پری نیچے</string>
<string name="post_down">پوسٹ نیچے</string>
<string name="quick_actions">فوری اقدامات</string>
<string name="advanced_settings">اعلی درجے کی ترتیبات</string>
<string name="debounce_delay">ڈیباؤنس میں تاخیر</string>
<string name="hide_amnezia_properties">Amnezia کی خصوصیات کو چھپائیں۔</string>
<string name="exclude_lan">LAN کو خارج کریں۔</string>
<string name="include_lan">LAN شامل کریں۔</string>
<string name="advanced_settings">اعلیٰ ترتیبات</string>
<string name="debounce_delay">ڈی باؤنس تاخیر</string>
<string name="hide_amnezia_properties">ایمنیزیا خصوصیات چھپائیں</string>
<string name="exclude_lan">لین خارج کریں</string>
<string name="include_lan">لین شامل کریں</string>
<string name="auto_tunnel">خودکار ٹنل</string>
<string name="kill_switch_off">قابل اعتماد پر کِل سوئچ کو بند کریں۔</string>
<string name="server_ipv4">IPv4 میزبان نام کی قرارداد</string>
<string name="kill_switch_off">قابل اعتماد پر کِل سوئچ بند کریں</string>
<string name="server_ipv4">آئی پی وی 4 ہوسٹ نیم ریزولیوشن</string>
<string name="multiple">متعدد</string>
<string name="prominent_background_location_message">اس فیچر کو ایپلیکیشن بند ہونے کے باوجود بھی Wi-Fi SSID مانیٹرنگ کو فعال کرنے کے لیے پس منظر کی جگہ کی اجازت درکار ہے۔ مزید تفصیلات کے لیے، براہ کرم سپورٹ اسکرین پر منسلک رازداری کی پالیسی دیکھیں۔</string>
<string name="prominent_background_location_message">اس فیچر کو ایپلیکیشن بند ہونے کے باوجود بھی وای فائی ایس ایس آی ڈی مانیٹرنگ کو فعال کرنے کے لیے پس منظر لوکیشن کی اجازت درکار ہے۔ مزید تفصیلات کے لیے، براہ کرم سپورٹ اسکرین پر منسلک رازداری کی پالیسی دیکھیں۔</string>
<string name="email_chooser">ای میل بھیجیں…</string>
<string name="add_tunnels_text">فائل یا زپ سے شامل کریں</string>
<string name="config_changes_saved">کنفیگریشن تبدیلیاں محفوظ ہو گئیں۔</string>
<string name="auto_tunneling">خودکار ٹنلنگ</string>
<string name="create_import">شروع سے تخلیق کریں۔</string>
<string name="create_import">شروع سے تخلیق کریں</string>
<string name="unknown_error">نامعلوم خرابی پیش آگئی</string>
<string name="seconds">سیکنڈ</string>
<string name="persistent_keepalive">مسلسل زندہ رہنا</string>
<string name="remove_amnezia_compatibility">Amnezia مطابقت کو ہٹا دیں۔</string>
<string name="light">روشنی</string>
<string name="set_ethernet_tunnel">ایتھرنیٹ ٹنل کے طور پر سیٹ کریں</string>
<string name="launch_app_settings">ایپ کی ترتیبات شروع کریں۔</string>
<string name="persistent_keepalive">مسلسل زندہ</string>
<string name="remove_amnezia_compatibility">ایمنیزیا مطابقت ہٹائیں</string>
<string name="light">روشن</string>
<string name="set_ethernet_tunnel">بطور ایتھرنیٹ ٹنل کے سیٹ کریں</string>
<string name="launch_app_settings">ایپ ترتیبات لانچ کریں</string>
<string name="monitoring_state_changes">ریاستی تبدیلیوں کی نگرانی</string>
<string name="set_custom_ping_internal">پنگ وقفہ (سیکنڈ)</string>
<string name="wifi_name_via_shell">شیل کے ذریعے وائی فائی کا نام</string>
<string name="enter_pin">اپنا پن درج کریں۔</string>
<string name="enter_pin">اپنا پن درج کریں</string>
<string name="getting_started_guide">شروع کرنے کی گائیڈ</string>
<string name="set_custom_ping_cooldown">پنگ دوبارہ شروع کول ڈاؤن (سیکنڈ)</string>
<string name="handshake">مصافحہ</string>
<string name="set_custom_ping_cooldown">پنگ دوبارہ شروع کولڈاؤن (سیکنڈ)</string>
<string name="handshake">ہینڈشیک</string>
<string name="response_packet_magic_header">رسپانس پیکٹ میجک ہیڈر</string>
<string name="unsure_how">اگر آپ کو یقین نہیں ہے کہ کیسے آگے بڑھیں۔</string>
<string name="unsure_how">اگر آپ کو یقین نہیں کہ کیسے آگے بڑھیں</string>
<string name="use_tunnel_on_wifi_name">وائی فائی نام پر ٹنل استعمال کریں</string>
<string name="optional_default">"اختیاری، ڈیفالٹ: "</string>
<string name="use_root_shell_for_wifi">وائی فائی کا نام حاصل کرنے کے لیے روٹ شیل کا استعمال کریں۔</string>
<string name="response_packet_junk_size">رسپانس پیکٹ ردی کا سائز</string>
<string name="optional_default">"اختیاری، طہ شدہ: "</string>
<string name="use_root_shell_for_wifi">وائی فائی کا نام حاصل کرنے کے لیے روٹ شیل کا استعمال کریں</string>
<string name="response_packet_junk_size">رسپانس پیکٹ جنک سائز</string>
<string name="junk_packet_minimum_size">جنک پیکٹ کا کم از کم سائز</string>
<string name="mobile_data_tunnel">موبائل ڈیٹا ٹنل کے طور پر سیٹ کریں</string>
<string name="edit_tunnel">ٹنل میں ترمیم کریں</string>
<string name="enable_app_lock">ایپ لاک کو فعال کریں۔</string>
<string name="logs">نوشتہ جات</string>
<string name="enable_app_lock">ایپ لاک فعال کریں</string>
<string name="logs">لاگز</string>
<string name="kernel">کرنل</string>
<string name="restart_at_boot">بوٹ پر دوبارہ شروع کریں۔</string>
<string name="support">حمایت</string>
<string name="background_location_message2">یہ یقینی بنانے کے لیے کہ یہ اجازتیں فعال ہیں۔</string>
<string name="restart_at_boot">بوٹ پر دوبارہ شروع کریں</string>
<string name="support">سپورٹ</string>
<string name="background_location_message2">یہ یقینی بنانے کے لیے کہ یہ اجازتیں فعال ہیں</string>
<string name="settings">ترتیبات</string>
<string name="junk_packet_maximum_size">جنک پیکٹ زیادہ سے زیادہ سائز</string>
<string name="enable_amnezia_compatibility">Amnezia مطابقت کو فعال کریں۔</string>
<string name="junk_packet_maximum_size">جنک پیکٹ کا زیادہ سے زیادہ سائز</string>
<string name="enable_amnezia_compatibility">ایمنیزیا مطابقت فعال کریں</string>
<string name="kill_switch">کِل سوئچ</string>
<string name="prefer_ipv4">IPv4 کنکشن کو ترجیح دیں۔</string>
<string name="native_kill_switch">مقامی قتل سوئچ</string>
<string name="prefer_ipv4">آئی پی وی 4 کنکشن کو ترجیح دیں</string>
<string name="native_kill_switch">مقامی کِل سوئچ</string>
<string name="auto_tunnel_channel_description">خودکار ٹنل اسٹیٹ کی اطلاعات کے لیے ایک چینل</string>
<string name="use_wildcards">نام وائلڈ کارڈ استعمال کریں۔</string>
<string name="use_wildcards">نام وائلڈ کارڈ استعمال کریں</string>
<string name="stop_auto">خودکار ٹنل روکیں</string>
<string name="add_wifi_name">وائی فائی کا نام شامل کریں۔</string>
<string name="add_wifi_name">وائی فائی کا نام شامل کریں</string>
<string name="wildcards_active">وائلڈ کارڈز فعال</string>
<string name="bypass_lan_for_kill_switch">کِل سوئچ کے لیے LAN کو بائی پاس کریں۔</string>
<string name="stop">روکو</string>
<string name="bypass_lan_for_kill_switch">کِل سوئچ کے لیے لین کو بائی پاس کریں</string>
<string name="stop">روکیں</string>
<string name="never">کبھی نہیں</string>
<string name="underload_packet_magic_header">پیکٹ میجک ہیڈر کو انڈر لوڈ کریں۔</string>
<string name="underload_packet_magic_header">پیکٹ میجک ہیڈر کو انڈر لوڈ کریں</string>
<string name="background_location_message">اس خصوصیت کے لیے ہر وقت مقام کی اجازت اور/یا درست مقام کی ضرورت ہے۔ ملاحظہ فرمائیں</string>
<string name="always_on_message2">یہ یقینی بنانے کے لیے کہ ہمیشہ آن VPN دیگر تمام ایپس کے لیے بند ہے اور دوبارہ کوشش کریں۔</string>
<string name="always_on_message2">یہ یقینی بنانے کے لیے کہ ہمیشہ آن وی پی این دیگر تمام ایپس کے لیے بند ہے اور دوبارہ کوشش کریں</string>
<string name="ethernet_tunnel">ایتھرنیٹ ٹنل</string>
<string name="auto_tunnel_channel_name">خودکار ٹنل نوٹیفکیشن چینل</string>
<string name="tunnel_running">ٹنل چل رہا ہے</string>
<string name="tunnel_control">ٹنل کنٹرول</string>
<string name="vpn_channel_description">VPN ریاستی اطلاعات کے لیے ایک چینل</string>
<string name="hide_scripts">اسکرپٹ چھپائیں۔</string>
<string name="add_from_clipboard">کلپ بورڈ سے شامل کریں۔</string>
<string name="vpn_channel_description">وی پی این ریاستی اطلاعات کے لیے ایک چینل</string>
<string name="hide_scripts">اسکرپٹس چھپائیں</string>
<string name="add_from_clipboard">کلپ بورڈ سے شامل کریں</string>
<string name="remote_key_template">کلید: %1$s</string>
<string name="enable_remote_app_control">ریموٹ ایپ کنٹرول کو فعال کریں۔</string>
<string name="kernel_name_error">کرنل ماڈیول نام کی خرابی۔</string>
<string name="enable_remote_app_control">ریموٹ ایپ کنٹرول فعال کریں</string>
<string name="kernel_name_error">کرنل ماڈیول نام کی خرابی</string>
<string name="app_permission_title">ڈبلیو جی ٹنل کنٹرول برج</string>
<string name="app_permission_description">ٹنل اور خودکار ٹنل کی خصوصیات کو کنٹرول کریں۔</string>
<string name="add_from_url">یو آر ایل سے شامل کریں۔</string>
<string name="enter_config_url">ترتیب یو آر ایل درج کریں۔</string>
<string name="error_download_failed">کنفیگریشن ڈاؤن لوڈ کرنے میں ناکام</string>
<string name="save">محفوظ کریں۔</string>
<string name="search">تلاش کریں۔</string>
<string name="select">منتخب کریں۔</string>
<string name="join_telegram">ٹیلیگرام کمیونٹی میں شامل ہوں۔</string>
<string name="add_from_url">یو آر ایل سے شامل کریں</string>
<string name="enter_config_url">کنفِگ یو آر ایل درج کریں</string>
<string name="error_download_failed">کنفیگریشن ڈاؤن لوڈ ناکام</string>
<string name="save">محفوظ</string>
<string name="search">تلاش</string>
<string name="select">منتخب</string>
<string name="join_telegram">ٹیلیگرام کمیونٹی میں شامل ہوں</string>
<string name="matrix_url">https://matrix.to/#/#wg-tunnel-space:matrix.org</string>
<string name="dropdown">ڈراپ ڈاؤن</string>
<string name="add_tunnel">ٹنل شامل کریں</string>
<string name="export_logs">ذخیرہ شدہ نوشتہ جات برآمد کریں۔</string>
<string name="export_logs">ذخیرہ شدہ لاگز برآمد کریں</string>
<string name="copy">نقل</string>
<string name="info">معلومات</string>
<string name="export_tunnels_wireguard">وائر گارڈ کے بطور ٹنلز برآمد کریں</string>
<string name="delete">حذف کریں۔</string>
<string name="camera_permission_required">کیمرے کی اجازت درکار ہے۔</string>
<string name="export_failed">ایکسپورٹ ناکام ہو گیا۔</string>
<string name="export_tunnels_wireguard">ٹنلز بطور وائر گارڈ برآمد کریں</string>
<string name="delete">حذف</string>
<string name="camera_permission_required">کیمرے کی اجازت درکار ہے</string>
<string name="export_failed">برآمد ناکام</string>
<string name="tunnel_error_template">ٹنل اس کے ساتھ ناکام ہوگئی: %1$s</string>
<string name="wifi_name_template">فعال: %1$s</string>
<string name="config_error">کنفگریشن کی خرابی</string>
<string name="dns_resolve_error">ڈی این ایس ریزولوشن کی خرابی۔</string>
<string name="invalid_config_error">غلط_تشکیل_کی_خرابی۔</string>
<string name="config_error">کنفگریشن خرابی</string>
<string name="dns_resolve_error">ڈی این ایس ریزولوشن خرابی</string>
<string name="invalid_config_error">غلط_کنفگریشن_خرابی۔</string>
<string name="auth_error">غیر مجاز غلطی</string>
<string name="service_running_error">سروس نہیں چل رہی خرابی۔</string>
<string name="service_running_error">سروس نہیں چل رہی کی خرابی</string>
<string name="inactive">غیر فعال</string>
<string name="active">فعال</string>
<string name="status">حیثیت</string>
<string name="bio_not_supported">بایومیٹرکس تعاون یافتہ نہیں ہیں۔</string>
<string name="bio_not_created">بائیو میٹرکس نہیں بنائے گئے۔</string>
<string name="bio_update_required">بائیو میٹرک سیکیورٹی اپ ڈیٹ درکار ہے۔</string>
<string name="tunnel_starting">ٹنل چل رہی ہے</string>
<string name="status">سٹیٹس</string>
<string name="bio_not_supported">بایومیٹرکس تعاون یافتہ نہیں</string>
<string name="bio_not_created">بائیومیٹرکس نہیں بنائے گئے</string>
<string name="bio_update_required">بائیومیٹرک سیکیورٹی تازہ کاری درکار ہے</string>
<string name="tunnel_starting">ٹنل شروع ہو رہی ہے</string>
<string name="nothing_here_yet">ابھی تک یہاں کچھ نہیں!</string>
<string name="join_matrix">میٹرکس کمیونٹی میں شامل ہوں۔</string>
<string name="delete_logs">نوشتہ جات کو حذف اور صاف کریں۔</string>
<string name="export_tunnels_amnezia">ایمنیزیا کے طور پر ٹنلز برآمد کریں</string>
<string name="bio_subtitle">اپنی بائیو میٹرک اسناد کا استعمال کرتے ہوئے لاگ ان کریں۔</string>
<string name="bio_auth_title">بائیو میٹرک تصدیق</string>
<string name="share">شیئر کریں۔</string>
<string name="select_all">سبھی کو منتخب کریں۔</string>
<string name="check_for_update">اپ ڈیٹ کے لیے چیک کریں۔</string>
<string name="update_check_failed">اپ ڈیٹ چیک ناکام ہو گیا۔</string>
<string name="checking_for_update">اپ ڈیٹس کی جانچ ہو رہی ہے۔</string>
<string name="update_download_failed">اپ ڈیٹ ڈاؤن لوڈ ناکام ہو گیا۔</string>
<string name="update_available">اپ ڈیٹ دستیاب ہے!</string>
<string name="download_and_install">ڈاؤن لوڈ اور انسٹال کریں۔</string>
<string name="permission_required">اجازت درکار ہے۔</string>
<string name="install_updated_permission">اس ایپ کو اپ ڈیٹس انسٹال کرنے کے لیے اجازت درکار ہے۔</string>
<string name="allow">اجازت دیں۔</string>
<string name="join_matrix">میٹرکس کمیونٹی میں شامل ہوں</string>
<string name="delete_logs">لاگز حذف اور صاف کریں</string>
<string name="export_tunnels_amnezia">ٹنلز بطور ایمنیزیا برآمد کریں</string>
<string name="bio_subtitle">اپنی بائیومیٹرک اسناد کا استعمال کرتے ہوئے لاگ ان کریں</string>
<string name="bio_auth_title">بائیومیٹرک تصدیق</string>
<string name="share">اشتراک</string>
<string name="select_all">سبھی کو منتخب کریں</string>
<string name="check_for_update">تازہ کاری کے لیے جانچ کریں</string>
<string name="update_check_failed">تازہ کاری کی جانچ ناکام ہو گئی۔</string>
<string name="checking_for_update">تازہ کاری کی جانچ ہو رہی ہے</string>
<string name="update_download_failed">تازہ کاری ڈاؤن لوڈ ناکام ہو گیا۔</string>
<string name="update_available">تازہ کاری دستیاب ہے!</string>
<string name="download_and_install">ڈاؤن لوڈ اور انسٹال کریں</string>
<string name="permission_required">اجازت درکار</string>
<string name="install_updated_permission">اس ایپ کو تازہ کاریاں انسٹال کرنے کے لیے اجازت درکار ہے۔</string>
<string name="allow">اجازت دیں</string>
<string name="licenses">لائسنس</string>
<string name="download">ڈاؤن لوڈ کریں۔</string>
<string name="download">ڈاؤن لوڈ</string>
<string name="version_template">ورژن: %1$s</string>
<string name="export_success">برآمد کی کامیابی</string>
<string name="export_success">برآمد کامیاب</string>
<string name="latest_installed">آپ پہلے ہی تازہ ترین ورژن چلا رہے ہیں۔</string>
<string name="darker">گہرا</string>
<string name="darker">بہت گہرا</string>
<string name="amoled">ایمولیڈ</string>
<string name="security_template">سیکیورٹی: %1$s</string>
<string name="update_check_unsupported">اپ ڈیٹ چیک اس قسم کی تعمیر کی حمایت نہیں کرتا ہے۔</string>
<string name="update_check_unsupported">تازہ کاری معائنہ اس قسم کی تعمیر کی حمایت نہیں کرتا ہے۔</string>
<string name="flavor_template">ذائقہ: %1$s</string>
<string name="show_qr">کیو آر دکھائیں</string>
<string name="amnezia">ایمنیزیا</string>
<string name="wireguard">وائرگارڈ</string>
<string name="done">ہوگیا</string>
</resources>
+50 -6
View File
@@ -38,8 +38,8 @@
<string name="delete_tunnel">刪除隧道</string>
<string name="delete_tunnel_message">您確定要刪除此隧道?</string>
<string name="yes"></string>
<string name="no_browser_detected">未安裝瀏覽器</string>
<string name="no_email_detected">未安裝電子郵件應用程式</string>
<string name="no_browser_detected">找不到瀏覽器</string>
<string name="no_email_detected">找不到電子郵件應用程式</string>
<string name="open_issue">建立新的問題</string>
<string name="auto">(自動)</string>
<string name="create_pin">建立 PIN</string>
@@ -55,7 +55,7 @@
<string name="getting_started_guide">取得入門指南</string>
<string name="settings">設定</string>
<string name="restart_at_boot">開機時重新啟動</string>
<string name="junk_packet_count">無效封包數</string>
<string name="junk_packet_count">垃圾封包數</string>
<string name="set_custom_ping_internal">Ping 間隔 (秒)</string>
<string name="app_settings">應用程式設定</string>
<string name="logs">日誌</string>
@@ -82,12 +82,12 @@
<string name="include_lan">包含 LAN</string>
<string name="unknown_error">發生未知錯誤</string>
<string name="error_file_format">無效的隧道組態檔案格式</string>
<string name="endpoint">端點</string>
<string name="endpoint">端點</string>
<string name="location_services_not_detected">未啟用定位服務</string>
<string name="junk_packet_maximum_size">無效封包最大大小</string>
<string name="junk_packet_maximum_size">垃圾封包最大</string>
<string name="no_tunnels">還沒有新增任何隧道!</string>
<string name="allowed_ips">允許的 IP</string>
<string name="junk_packet_minimum_size">無效封包最小大小</string>
<string name="junk_packet_minimum_size">垃圾封包最小</string>
<string name="error_no_file_explorer">未安裝任何檔案管理器</string>
<string name="unsure_how">如果您不確定如何進行</string>
<string name="see_the">請參閱</string>
@@ -176,4 +176,48 @@
<string name="amoled">AMOLED</string>
<string name="update_check_unsupported">更新檢查不支援此建置類型。</string>
<string name="darker">黑色</string>
<string name="preshared_key">預分享金鑰</string>
<string name="error_authentication_failed">驗證失敗</string>
<string name="transport_packet_magic_header">傳輸封包魔法標頭</string>
<string name="default_ping_ip">(可選,預設選擇端點)</string>
<string name="tunnel_running">隧道運作中</string>
<string name="hide_scripts">隱藏指令碼</string>
<string name="underload_packet_magic_header">負載封包魔法標頭</string>
<string name="email_chooser">發送電子郵件…</string>
<string name="handshake">上次交握時間</string>
<string name="vpn_channel_description">VPN 狀態通知頻道</string>
<string name="auto_tunnel_channel_name">自動隧道管理 通知頻道</string>
<string name="show_scripts">顯示指令碼</string>
<string name="init_packet_junk_size">初始封包垃圾大小</string>
<string name="response_packet_junk_size">回應封包垃圾大小</string>
<string name="auto_tunnel_channel_description">自動隧道管理服務狀態 通知頻道</string>
<string name="prominent_background_location_message">此功能需要背景位置權限以啟用即使應用程式關閉也會持續執行的 Wi-Fi SSID 自動掃描。如果想知道更多資訊,請參閱支援頁面的隱私政策。</string>
<string name="tunnel_starting">隧道正在啟動</string>
<string name="error_authorization_failed">授權失敗</string>
<string name="vpn_channel_name">VPN 通知頻道</string>
<string name="peer">端點</string>
<string name="turn_off_tunnel">此操作需要關閉隧道</string>
<string name="turn_on_tunnel">此操作需要一個已建立的隧道</string>
<string name="rotate_keys">輪換金鑰</string>
<string name="add_peer">新增端點</string>
<string name="init_packet_magic_header">初始封包魔法標頭</string>
<string name="response_packet_magic_header">回應封包魔法標頭</string>
<string name="background_location_message">此功能需要任何時間都被允許使用位置權限或者精確位置。請參閱</string>
<string name="always_on_message2">確保 永久連線的 VPN 已經關閉並再試一次</string>
<string name="enter_config_url">輸入組態 URL</string>
<string name="error_download_failed">下載組態失敗</string>
<string name="config_error">組態錯誤</string>
<string name="invalid_config_error">組態無效</string>
<string name="remote_key_template">金鑰: %1$s</string>
<string name="flavor_template">風格: %1$s</string>
<string name="security_template">安全性: %1$s</string>
<string name="amnezia">Amnezia</string>
<string name="wireguard">WireGuard</string>
<string name="done">完成</string>
<string name="show_qr">顯示 QR code</string>
<string name="optional_default">"可選,預設: "</string>
<string name="pre_up">啟動前</string>
<string name="post_up">啟動後</string>
<string name="pre_down">關閉前</string>
<string name="post_down">關閉後</string>
</resources>
+4
View File
@@ -281,4 +281,8 @@
</string>
<string name="release_notes">Release notes</string>
<string name="shizuku_not_detected">Shizuku not detected</string>
<string name="sort">Sort</string>
<string name="drag_handle">Drag Handle</string>
<string name="move_up">Move Up</string>
<string name="move_down">Move Down</string>
</resources>
@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions specific to full -->
<!-- Permissions specific to standalone -->
<!--updater-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
</manifest>
+1 -1
View File
@@ -9,5 +9,5 @@ repositories {
dependencies {
implementation("org.semver4j:semver4j:5.7.0")
implementation("org.ajoberstar.grgit:grgit-core:5.3.0")
implementation("org.ajoberstar.grgit:grgit-core:5.3.2")
}
+4 -3
View File
@@ -1,7 +1,7 @@
object Constants {
const val VERSION_NAME = "3.9.3"
const val VERSION_NAME = "3.9.4"
const val JVM_TARGET = "17"
const val VERSION_CODE = 39300
const val VERSION_CODE = 39400
const val TARGET_SDK = 35
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -13,5 +13,6 @@ object Constants {
const val PRERELEASE = "prerelease"
val allowedLicenses = listOf("MIT", "Apache-2.0", "BSD-3-Clause")
val allowedLicenseUrls = listOf("https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING", "https://github.com/RikkaApps/Shizuku-API/blob/master/LICENSE")
val allowedLicenseUrls = listOf("https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING",
"https://github.com/RikkaApps/Shizuku-API/blob/master/LICENSE")
}
@@ -0,0 +1 @@
برنامج عميل VPN لبروتوكول Wireguard مع خصائص إضافية
+1
View File
@@ -0,0 +1 @@
WG Tunnel
@@ -0,0 +1,6 @@
What's new:
- Tunnel sorting
- Shizuku support for Wi-Fi SSIDs
- Android TV hover visibility improvements
- Auto-tunnel default detection method bug fix
- Other UI changes and improvements
@@ -0,0 +1,6 @@
Mida on meil uut:
- nimelahenduse kontekstis tunneli ühenduvuse puudumise vea parandus
- tunneli lõputu käivitamise vea parandus
- tunneli infovaates on nüüd QR-kood
- otspunkt on nüüd leitav statistikast
- erinevad muud veaparandused
@@ -0,0 +1,14 @@
Funktsionaalsused:
- Tunneli lisamine .conf faili, .zip faili, käsitsi sisestuse või QR-koodi alusel
- VPN-i automaatne ühendamine konkreetse WiFi SSID, traadiga võrgu või mobiilse andmeside alusel
- Jagatud tunneldus koos otsinguga
- WireGuardi tugi nii kerneli, kui kasutajarežiimis
- Amnezia tugi kasutajarežiimis nii pakettide süvakontrolli kui tsenseerimise vastu
- Alati toimiva VPN-i tugi
- Amnezia ja WireGuardi tunnelite eksport zip-faili
- Androidi kiirvalikute tugi VPN-i sisselülitamiseks või katkestamiseks
- Põhitunneli staatiliste linkide tugi automaatseks lõimimiseks
- Sinu määratud eelistuste automatiseerimise tugi kõikide tunnelite puhul
- Teenuse käivitamine koos nutiseadme käivitamisega
- Säästlik akukasutus
@@ -0,0 +1 @@
WireGuardil põhineva VPN-i alternatiivne klientrakendus lisavõimalustega
+1
View File
@@ -0,0 +1 @@
WG Tunnel
@@ -0,0 +1,6 @@
تغییرات:
- پشتیبانی از تونل چندگانه برای حالت هسته
- لغو تنظیمات پیش‌فرض WG برای DNS Ipv4
- کیل سویچ در برخی سیستم های قابلیت اعتماد
- محدود کردن جستجوی موقعیت مکانی توسط تونل خودکار
- رفع اشکالات و بهبودهای مختلف
@@ -0,0 +1,5 @@
تغییرات :
- رفع اشکال نقطه پایانی همتا IPv6 استاتیک
- رفع اشکال میانبرهای داینامیک
- محلی سازی ها
- رفع اشکال اضافه کردن همتا
@@ -0,0 +1,5 @@
تغییرات:
- رفع مشکل تونل خودکار
- بهبود همگام‌سازی تایل
- بهینه‌سازی جستجوی نام وای‌فای
- بهبود بررسی مجوزهای نظارت بر شبکه
@@ -0,0 +1,8 @@
تغییرات:
- به‌روزرسانی رابط کاربری
- اضافه کردن تونل از طریق URL
- رفع اشکال سوئیچ kill VPN
- رفع خرابی سرویس تونل
- رفع مشکلات راه‌اندازی مجدد تغییر پینگ و پیکربندی
- اطلاعات شبکه در رابط کاربری
- رفع مشکلات و بهبودهای مختلف دیگر
@@ -0,0 +1,6 @@
تغییرات:
- یکپارچه‌سازی جدید با قابلیت کنترل از راه دور برای برنامه
- کپی کردن نام وای‌فای فعال در کلیپ‌بورد
- رفع اشکال خاموش شدن تونل
- رفع اشکال سرور پیش‌فرض DNS، ترجیح DNS تلفن
- رفع اشکال پیکربندی هاست همتا داینامیک Ipv6
@@ -0,0 +1,8 @@
تغییرات:
- آیکون جدید برنامه و بهبودهای جزئی رابط کاربری
- رفع اشکال تغییر وضعیت ثبت‌کننده محلی
- رفع مشکل راه‌اندازی مجدد AndroidTV پس از راه‌اندازی مجدد
- رفع مشکل انتخاب زبان AndroidTV
- رفع مشکل اجرای پیش‌فرض ویژگی پینگ
- رفع اشکال خاموش شدن در حالت هسته
- رفع اشکال تونل‌سازی خودکار kill switch
@@ -0,0 +1,6 @@
تغییرات:
- رفع مشکلات مربوط به وضوح DNS
- بهبود بازیابی تلویزیون اندروید در حالت خواب
- رفع اشکال جستجوی تونل تقسیم شده
- به‌روزرسانی‌های محلی‌سازی
- به‌روزرسانی لینک‌های اسناد برای وب‌سایت جدید برنامه
@@ -0,0 +1,6 @@
تغییرات:
- رفع مشکل بنرهای تلویزیون اندروید
- اضافه شدن قابلیت انتخاب چندگانه برای تونل‌ها
- اضافه شدن بررسی‌کننده به‌روزرسانی درون‌برنامه‌ای
- اضافه شدن صفحه مجوز
- رفع اشکالات مختلف
@@ -0,0 +1,6 @@
تغییرات:
- رفع اشکال DNS عدم اتصال تونل
- رفع اشکال شروع گیر کردن تونل
- اضافه کردن کد QR تونل نمای
- اضافه کردن نقطه پایانی حل شده به آمار
- رفع اشکالات مختلف دیگر
@@ -0,0 +1,14 @@
ویژگی‌ها
- اضافه کردن تونل‌ها از طریق فایل .conf، زیپ، ورود دستی یا کد QR
- اتصال خودکار به VPN بر اساس SSID وای‌فای، اترنت یا داده تلفن همراه
- تقسیم تونل‌سازی توسط برنامه با جستجو
- پشتیبانی از WireGuard برای حالت‌های هسته و فضای کاربری
- پشتیبانی از Amnezia برای حالت فضای کاربری برای محافظت در برابر DPI/سانسور
- پشتیبانی از VPN همیشه روشن
- خروجی گرفتن از تونل‌های Amnezia و WireGuard به فایل زیپ
- پشتیبانی از تغییر سریع وضعیت VPN
- پشتیبانی از میانبرهای استاتیک برای تونل اصلی برای ادغام اتوماسیون
- پشتیبانی از اتوماسیون Intent برای همه تونل‌ها
- راه‌اندازی مجدد خودکار سرویس پس از راه‌اندازی مجدد اپلیکیشن
- اقدامات حفظ باتری
@@ -0,0 +1 @@
برنامه ای برای جایگزینی وایرگارد با امکانات بیشتر
@@ -0,0 +1 @@
وایرگارد تانل
@@ -0,0 +1,6 @@
Nouveautés :
- Support de plusieurs tunnels pour le mode noyau
- Remplacement de la préférence DNS Ipv4 par défaut de WG
- Arrêter le kill switch sur le support de confiance
- Limitation de l'interrogation de l'emplacement par tunnel automatique
- Diverses corrections de bogues et améliorations
@@ -0,0 +1,5 @@
Nouveautés :
- Correction d'un bogue concernant le point d'extrémité du pair IPv6 statique
- Correction du bogue des raccourcis dynamiques
- Localisations
- Correction du bogue de l'ajout d'un pair
@@ -0,0 +1,5 @@
Nouveautés :
- Correction de la régression du tunnel automatique
- Amélioration de la synchronisation des tuiles
- Optimisation de l'interrogation du nom du wifi
- Amélioration des vérifications de permission pour la surveillance du réseau
@@ -0,0 +1,8 @@
Nouveautés :
- Mise à jour de l'interface utilisateur
- Ajouter un tunnel via l'URL
- Correction des bugs du VPN kill switch
- Correction des crashs du service tunnel
- Correction du ping et du redémarrage en cas de changement de configuration
- Informations sur le réseau dans l'interface utilisateur
- Diverses autres corrections et améliorations
@@ -0,0 +1 @@
Alternativna VPN klijent aplikacija za WireGuard sa dodatnim funkcijama
+1
View File
@@ -0,0 +1 @@
WG Tunnel
@@ -1,6 +1,6 @@
نیا کیا ہے:
- کرنل موڈ کے لیے متعدد ٹنل سپورٹ
- WG ڈیفالٹ DNS Ipv4 ترجیح کے لیے اوور رائیڈ
- ڈبلیو جی ڈیفالٹ ڈی این ایس آی پی وی 4 ترجیح کے لیے اوور رائیڈ
- قابل اعتماد سپورٹ پر کِل سوئچ کو روکیں۔
- آٹو ٹنل کے ذریعہ مقام کے استفسار کو محدود کریں۔
- مختلف بگ کی اصلاحات اور بہتری
@@ -0,0 +1,6 @@
نیا کیا ہے:
- ٹنل بغیر کنیکٹیویٹی DNS بگ کو درست کریں۔
- ٹنل میں پھنسے شروع ہونے والے بگ کو درست کریں۔
- ویو ٹنل کیو آر کوڈ شامل کریں۔
- شماریات میں حل شدہ اختتامی نقطہ شامل کریں۔
- مختلف دیگر بگ فکسز
@@ -1,14 +1,14 @@
خصوصیات
- .conf فائل، زپ، دستی اندراج، یا QR کوڈ کے ذریعے سرنگیں شامل کریں
- Wi-Fi SSID، ایتھرنیٹ، یا موبائل ڈیٹا کی بنیاد پر VPN سے خودکار جڑیں۔
- تلاش کے ساتھ ایپلی کیشن کے ذریعہ سرنگ کو تقسیم کریں۔
- .conf فائل، زپ، دستی اندراج، یا کیو آر کوڈ کے ذریعے ٹنلز شامل کریں
- وائی فائی ایس ایس آی ڈی، ای تھرنیٹ، یا موبائل ڈیٹا کی بنیاد پر وی پی این سے خودکار جڑیں
- تلاش کے ساتھ ایپلی کیشن کے ذریعہ ٹنل کو تقسیم کریں
- کرنل اور یوزر اسپیس موڈز کے لیے وائر گارڈ سپورٹ
- ڈی پی آئی/سینسرشپ کے تحفظ کے لیے یوزر اسپیس موڈ کے لیے ایمنیزیا سپورٹ
- ہمیشہ آن VPN سپورٹ
- ایمنیزیا اور وائر گارڈ سرنگوں کو زپ میں ایکسپورٹ کریں۔
- VPN ٹوگلنگ کے لیے فوری ٹائل سپورٹ
- آٹومیشن انضمام کے لیے بنیادی سرنگ کے لیے جامد شارٹ کٹ سپورٹ
- تمام سرنگوں کے لئے ارادہ آٹومیشن سپورٹ
- ہمیشہ آن وی پی این سپورٹ
- ایمنیزیا اور وائر گارڈ ٹنلز کو زپ میں ایکسپورٹ کریں
- وی پی این ٹوگلنگ کے لیے فوری ٹائل سپورٹ
- آٹومیشن انضمام کے لیے بنیادی ٹنلز کے لیے جامد شارٹ کٹ سپورٹ
- تمام ٹنلز کے لئے ارادہ آٹومیشن سپورٹ
- ریبوٹ کے بعد خودکار سروس دوبارہ شروع
- بیٹری کے تحفظ کے اقدامات
@@ -1 +1 @@
اضافی خصوصیات کے ساتھ WireGuard کے لیے ایک متبادل VPN کلائنٹ ایپ
اضافی خصوصیات کے ساتھ وائرگارڈ کے لیے ایک متبادل وی پی این کلائنٹ ایپ
+1
View File
@@ -0,0 +1 @@
WG Tunnel
@@ -1 +1 @@
一款具備進階功能的 WireGuard VPN 戶端應用程式
一款具備進階功能的 WireGuard VPN 戶端應用程式
+7 -5
View File
@@ -1,7 +1,7 @@
[versions]
accompanist = "0.37.3"
activityCompose = "1.10.1"
amneziawgAndroid = "1.4.0"
amneziawgAndroid = "1.5.0"
androidx-junit = "1.2.1"
shizuku = "13.1.5"
appcompat = "1.7.1"
@@ -24,17 +24,18 @@ roomVersion = "2.7.1"
semver4j = "3.1.0"
slf4jAndroid = "1.7.36"
timber = "5.0.1"
tunnel = "1.3.0"
tunnel = "1.4.0"
androidGradlePlugin = "8.10.1"
kotlin = "2.1.21"
ksp = "2.1.21-2.0.2"
kotlin = "2.2.0"
ksp = "2.2.0-2.0.2"
composeBom = "2025.06.00"
compose = "1.8.2"
icons = "1.7.8"
workRuntimeKtxVersion = "2.10.1"
zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1"
gradlePlugins-grgit = "5.3.0"
gradlePlugins-grgit = "5.3.2"
reorderable = "2.5.1"
#plugins
material = "1.12.0"
@@ -110,6 +111,7 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" }
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
# tunnel
tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel" }
@@ -0,0 +1,32 @@
package com.zaneschepke.networkmonitor
import android.net.Network
import android.net.NetworkCapabilities
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
// keep track of the currently active network(s)
class ActiveWifiStateManager {
private val _stateFlow =
MutableStateFlow(linkedMapOf<String, Pair<Network?, NetworkCapabilities?>>())
@Synchronized
fun put(key: String, value: Pair<Network?, NetworkCapabilities?>) {
_stateFlow.update { currentMap ->
linkedMapOf(*currentMap.toList().toTypedArray()).apply { put(key, value) }
}
}
@Synchronized
fun remove(key: String) {
_stateFlow.update { currentMap ->
linkedMapOf(*currentMap.toList().toTypedArray()).apply { remove(key) }
}
}
fun isEmpty(): Boolean = _stateFlow.value.isEmpty()
fun getLatestValue(): Pair<Network?, NetworkCapabilities?>? {
return _stateFlow.value.entries.lastOrNull()?.value
}
}
@@ -1,9 +1,11 @@
package com.zaneschepke.networkmonitor
import android.Manifest
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.location.LocationManager
import android.net.ConnectivityManager
import android.net.Network
@@ -11,13 +13,17 @@ import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.os.Build
import androidx.core.content.ContextCompat
import com.wireguard.android.util.RootShell
import com.zaneschepke.networkmonitor.shizuku.ShizukuShell
import com.zaneschepke.networkmonitor.util.WIFI_SSID_SHELL_COMMAND
import com.zaneschepke.networkmonitor.util.getCurrentSecurityType
import com.zaneschepke.networkmonitor.util.getCurrentWifiName
import com.zaneschepke.networkmonitor.util.getWifiSsid
import com.zaneschepke.networkmonitor.util.isLocationServicesEnabled
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
class AndroidNetworkMonitor(
@@ -45,76 +51,31 @@ class AndroidNetworkMonitor(
companion object {
fun fromValue(value: Int): WifiDetectionMethod =
WifiDetectionMethod.entries.find { it.value == value } ?: DEFAULT
entries.find { it.value == value } ?: DEFAULT
}
}
private val packageName = appContext.packageName
private val connectivityManager =
appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
private val locationManager =
appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
private val wifiMutex = Mutex()
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
private var currentSsid: String? = null
private var securityType: WifiSecurityType? = null
private var wifiConnected = false
// Track active Wi-Fi networks and last active network ID
private val activeWifiNetworks = mutableSetOf<String>()
data class WifiState(
val connected: Boolean = false,
val ssid: String? = null,
val securityType: WifiSecurityType? = null,
)
data class TransportState(val connected: Boolean = false)
// Track active Wi-Fi networks, their capabilities, and last active network ID
private val activeWifiNetworks = ActiveWifiStateManager()
@OptIn(ExperimentalCoroutinesApi::class)
private val wifiFlow: Flow<WifiState> =
private val wifiFlow: Flow<TransportEvent> =
configurationListener.detectionMethod.flatMapLatest { detectionMethod
-> // cancels previous flow
Timber.d("Updated detectionMethod=$detectionMethod, recreating wifiFlow")
createWifiNetworkCallbackFlow(detectionMethod) // Create a new flow for each new method
}
private fun createWifiNetworkCallbackFlow(
detectionMethod: WifiDetectionMethod
): Flow<WifiState> = callbackFlow {
@Suppress("DEPRECATION")
suspend fun getWifiSsid(): String {
return withContext(ioDispatcher) {
if (wifiManager == null) return@withContext ANDROID_UNKNOWN_SSID
try {
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
?: ANDROID_UNKNOWN_SSID
} catch (e: Exception) {
Timber.e(e)
ANDROID_UNKNOWN_SSID
}
}
}
suspend fun handleUnknownWifi() {
wifiMutex.withLock {
val newSsid = getWifiSsid()
val securityType = wifiManager?.getCurrentSecurityType()
// Only update if new SSID is valid; preserve existing valid SSID otherwise
if (newSsid != WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(wifiConnected, currentSsid, securityType))
} else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(wifiConnected, currentSsid, securityType))
}
}
}
): Flow<TransportEvent> = callbackFlow {
val locationPermissionReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@@ -125,7 +86,15 @@ class AndroidNetworkMonitor(
Timber.d(
"Received update: Precise and all-the-time location permissions are enabled"
)
applicationScope.launch { handleUnknownWifi() }
activeWifiNetworks.getLatestValue()?.let { details ->
trySend(
TransportEvent.LocationPermissionGranted(
details.first,
details.second,
detectionMethod,
)
)
}
}
}
}
@@ -135,23 +104,39 @@ class AndroidNetworkMonitor(
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == LOCATION_SERVICES_FILTER) {
val isGpsEnabled =
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER)
?: false
val isNetworkEnabled =
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
?: false
val isLocationServicesEnabled = isGpsEnabled || isNetworkEnabled
Timber.d(
"Location Services state changed. Enabled: $isLocationServicesEnabled, GPS: $isGpsEnabled, Network: $isNetworkEnabled"
)
if (isLocationServicesEnabled)
applicationScope.launch { handleUnknownWifi() }
activeWifiNetworks.getLatestValue()?.let { details ->
trySend(
TransportEvent.LocationServicesChanged(
isLocationServicesEnabled,
details.first,
details.second,
detectionMethod,
)
)
}
}
}
}
// Use RECEIVER_NOT_EXPORTED for Android 14+ compatibility
val flags =
val permissionReceiverFlags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Context.RECEIVER_NOT_EXPORTED // Internal broadcast
} else {
0
}
val servicesReceiverFlags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
Context.RECEIVER_EXPORTED
Context.RECEIVER_EXPORTED // System broadcast
} else {
0
}
@@ -159,64 +144,42 @@ class AndroidNetworkMonitor(
appContext.registerReceiver(
locationPermissionReceiver,
IntentFilter("$packageName.$LOCATION_GRANTED"),
flags,
permissionReceiverFlags,
)
appContext.registerReceiver(
locationServicesReceiver,
IntentFilter(LOCATION_SERVICES_FILTER),
flags,
servicesReceiverFlags,
)
suspend fun handleOnWifiLost(network: Network) {
wifiMutex.withLock {
Timber.d("Wi-Fi onLost: network=$network")
activeWifiNetworks.remove(network.toString())
if (activeWifiNetworks.isEmpty()) {
Timber.d(
"All Wi-Fi networks disconnected, clearing currentSsid and wifiConnected"
)
currentSsid = null
wifiConnected = false
trySend(WifiState(connected = false, ssid = null, securityType = null))
} else {
Timber.d("Wi-Fi onLost, but still connected to other networks, ignoring")
}
fun handleOnWifiLost(network: Network) {
Timber.d("Wi-Fi onLost: network=$network")
activeWifiNetworks.remove(network.toString())
if (activeWifiNetworks.isEmpty()) {
Timber.d("All Wi-Fi networks disconnected, clearing currentSsid and wifiConnected")
trySend(TransportEvent.Lost(network))
} else {
Timber.d("Wi-Fi onLost, but still connected to other networks, ignoring")
// This can happen when switching between APs of the same SSID
}
}
suspend fun handleOnWifiAvailable(
fun handleOnWifiAvailable(network: Network) {
Timber.d("Wi-Fi onAvailable: network=$network")
activeWifiNetworks.put(network.toString(), Pair(network, null))
trySend(TransportEvent.Available(network, detectionMethod))
}
fun handleOnWifiCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities?,
networkCapabilities: NetworkCapabilities,
) {
wifiMutex.withLock {
Timber.d("Wi-Fi onAvailable: network=$network")
activeWifiNetworks.add(network.toString())
currentSsid =
try {
when (detectionMethod) {
WifiDetectionMethod.DEFAULT ->
networkCapabilities?.getWifiSsid() ?: getWifiSsid()
WifiDetectionMethod.LEGACY -> getWifiSsid()
WifiDetectionMethod.ROOT ->
configurationListener.rootShell.getCurrentWifiName()
WifiDetectionMethod.SHIZUKU ->
ShizukuShell(applicationScope)
.singleResponseCommand(WIFI_SSID_SHELL_COMMAND)
}
.trim()
.replace(Regex("[\n\r]"), "")
} catch (e: Exception) {
Timber.e(e)
ANDROID_UNKNOWN_SSID
}
.also { Timber.d("Current SSID via ${detectionMethod.name}: $it") }
securityType = wifiManager?.getCurrentSecurityType()
wifiConnected = true
trySend(
WifiState(connected = true, ssid = currentSsid, securityType = securityType)
)
}
Timber.d("Wi-Fi onCapabilitiesChanged: network=$network")
activeWifiNetworks.put(network.toString(), Pair(network, networkCapabilities))
trySend(
TransportEvent.CapabilitiesChanged(network, networkCapabilities, detectionMethod)
)
}
val callback =
@@ -225,11 +188,11 @@ class AndroidNetworkMonitor(
Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
applicationScope.launch { handleOnWifiAvailable(network, null) }
handleOnWifiAvailable(network)
}
override fun onLost(network: Network) {
applicationScope.launch { handleOnWifiLost(network) }
handleOnWifiLost(network)
}
}
else ->
@@ -237,7 +200,7 @@ class AndroidNetworkMonitor(
override fun onAvailable(network: Network) {
if (detectionMethod != WifiDetectionMethod.DEFAULT)
applicationScope.launch { handleOnWifiAvailable(network, null) }
handleOnWifiAvailable(network)
}
override fun onCapabilitiesChanged(
@@ -245,13 +208,11 @@ class AndroidNetworkMonitor(
networkCapabilities: NetworkCapabilities,
) {
if (detectionMethod == WifiDetectionMethod.DEFAULT)
applicationScope.launch {
handleOnWifiAvailable(network, networkCapabilities)
}
handleOnWifiCapabilitiesChanged(network, networkCapabilities)
}
override fun onLost(network: Network) {
applicationScope.launch { handleOnWifiLost(network) }
handleOnWifiLost(network)
}
}
}
@@ -262,34 +223,31 @@ class AndroidNetworkMonitor(
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build()
connectivityManager.registerNetworkCallback(request, callback)
trySend(WifiState())
connectivityManager?.registerNetworkCallback(request, callback)
trySend(TransportEvent.Unknown)
awaitClose {
try {
connectivityManager.unregisterNetworkCallback(callback)
} catch (e: IllegalArgumentException) {
Timber.e(
e,
"Flow failed to unregister NetworkCallback, was already unregistered or not registered correctly.",
)
}
appContext.unregisterReceiver(locationPermissionReceiver)
appContext.unregisterReceiver(locationServicesReceiver)
runCatching {
appContext.unregisterReceiver(locationPermissionReceiver)
appContext.unregisterReceiver(locationServicesReceiver)
connectivityManager?.unregisterNetworkCallback(callback)
}
.onFailure { Timber.e(it, "Error unregistering network callback") }
}
}
private val cellularFlow: Flow<TransportState> = callbackFlow {
private val cellularFlow: Flow<TransportEvent> = callbackFlow {
val callback =
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Timber.d("Cellular onAvailable: network=$network")
trySend(TransportState(connected = true))
trySend(TransportEvent.Available(network))
}
override fun onLost(network: Network) {
Timber.d("Cellular onLost: network=$network")
trySend(TransportState(connected = false))
trySend(TransportEvent.Lost(network))
}
}
@@ -299,23 +257,26 @@ class AndroidNetworkMonitor(
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build()
connectivityManager.registerNetworkCallback(request, callback)
trySend(TransportState())
connectivityManager?.registerNetworkCallback(request, callback)
trySend(TransportEvent.Unknown)
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
awaitClose {
runCatching { connectivityManager?.unregisterNetworkCallback(callback) }
.onFailure { Timber.e(it, "Error unregistering cellular network callback") }
}
}
private val ethernetFlow: Flow<TransportState> = callbackFlow {
private val ethernetFlow: Flow<TransportEvent> = callbackFlow {
val callback =
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Timber.d("Ethernet onAvailable: network=$network")
trySend(TransportState(connected = true))
trySend(TransportEvent.Available(network))
}
override fun onLost(network: Network) {
Timber.d("Ethernet onLost: network=$network")
trySend(TransportState(connected = false))
trySend(TransportEvent.Lost(network))
}
}
@@ -325,35 +286,118 @@ class AndroidNetworkMonitor(
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.build()
connectivityManager.registerNetworkCallback(request, callback)
trySend(TransportState())
connectivityManager?.registerNetworkCallback(request, callback)
trySend(TransportEvent.Unknown)
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
awaitClose {
runCatching { connectivityManager?.unregisterNetworkCallback(callback) }
.onFailure { Timber.e(it, "Error unregistering ethernet network callback") }
}
}
override val networkStatusFlow =
combine(wifiFlow, cellularFlow, ethernetFlow) { wifi, cellular, ethernet ->
val hasAnyConnection = wifi.connected || cellular.connected || ethernet.connected
if (hasAnyConnection) {
NetworkStatus.Connected(
wifiSsid = wifi.ssid,
securityType = wifi.securityType,
wifiConnected = wifi.connected,
cellularConnected = cellular.connected,
ethernetConnected = ethernet.connected,
)
} else {
NetworkStatus.Disconnected
suspend fun getSsidByDetectionMethod(
detectionMethod: WifiDetectionMethod?,
networkCapabilities: NetworkCapabilities?,
): String {
val method = detectionMethod ?: WifiDetectionMethod.DEFAULT
return try {
when (method) {
WifiDetectionMethod.DEFAULT ->
networkCapabilities?.getWifiSsid()
?: wifiManager?.getWifiSsid()
?: ANDROID_UNKNOWN_SSID
WifiDetectionMethod.LEGACY ->
wifiManager?.getWifiSsid() ?: ANDROID_UNKNOWN_SSID
WifiDetectionMethod.ROOT ->
withTimeoutOrNull(2000) { // 2-second timeout
configurationListener.rootShell.getCurrentWifiName()
} ?: ANDROID_UNKNOWN_SSID
WifiDetectionMethod.SHIZUKU ->
withTimeoutOrNull(2000) { // 2-second timeout
ShizukuShell(applicationScope)
.singleResponseCommand(WIFI_SSID_SHELL_COMMAND)
} ?: ANDROID_UNKNOWN_SSID
}
.also { Timber.d("NetworkStatus: $it") }
.trim()
.replace(Regex("[\n\r]"), "")
} catch (e: Exception) {
Timber.e(e, "Failed to get SSID with method: ${method.name}")
ANDROID_UNKNOWN_SSID
}
.also { Timber.d("Current SSID via ${method.name}: $it") }
}
override val connectivityStateFlow =
combine(
wifiFlow.scan(
WifiState(
locationPermissionsGranted =
ContextCompat.checkSelfPermission(
appContext,
Manifest.permission.ACCESS_FINE_LOCATION,
) == PackageManager.PERMISSION_GRANTED,
locationServicesEnabled =
locationManager?.isLocationServicesEnabled() ?: false,
)
) { previous, event ->
when (event) {
is TransportEvent.Available ->
previous.copy(
connected = true,
ssid =
getSsidByDetectionMethod(
event.wifiDetectionMethod ?: WifiDetectionMethod.DEFAULT,
null,
),
securityType = wifiManager?.getCurrentSecurityType(),
)
is TransportEvent.CapabilitiesChanged ->
previous.copy(
connected = true,
ssid =
getSsidByDetectionMethod(
event.wifiDetectionMethod ?: WifiDetectionMethod.DEFAULT,
null,
),
securityType = wifiManager?.getCurrentSecurityType(),
)
is TransportEvent.LocationPermissionGranted ->
previous.copy(
locationPermissionsGranted = true,
ssid =
getSsidByDetectionMethod(
event.wifiDetectionMethod,
event.networkCapabilities,
),
securityType = wifiManager?.getCurrentSecurityType(),
)
is TransportEvent.LocationServicesChanged ->
previous.copy(
locationServicesEnabled = event.enabled,
ssid =
getSsidByDetectionMethod(
event.wifiDetectionMethod,
event.networkCapabilities,
),
securityType = wifiManager?.getCurrentSecurityType(),
)
is TransportEvent.Lost ->
previous.copy(connected = false, securityType = null, ssid = null)
TransportEvent.Unknown -> previous
}
},
cellularFlow,
ethernetFlow,
) { wifi, cellular, ethernet ->
val cellularConnected = cellular is TransportEvent.Available
val ethernetConnected = ethernet is TransportEvent.Available
ConnectivityState(
wifi,
cellularConnected = cellularConnected,
ethernetConnected = ethernetConnected,
)
.also { Timber.d("Connectivity Status: $it") }
}
.distinctUntilChanged()
.shareIn(applicationScope, SharingStarted.WhileSubscribed(5000), replay = 1)
override fun sendLocationPermissionsGrantedBroadcast() {
val action = "$packageName.$LOCATION_GRANTED"
val intent = Intent(action)
Timber.d("Sending broadcast: $action")
appContext.sendBroadcast(intent)
}
}
@@ -0,0 +1,19 @@
package com.zaneschepke.networkmonitor
import com.zaneschepke.networkmonitor.util.WifiSecurityType
data class ConnectivityState(
val wifiState: WifiState,
val ethernetConnected: Boolean = false,
val cellularConnected: Boolean = false,
) {
fun hasConnectivity(): Boolean = wifiState.connected || ethernetConnected || cellularConnected
}
data class WifiState(
val connected: Boolean = false,
val ssid: String? = null,
val securityType: WifiSecurityType? = null,
val locationPermissionsGranted: Boolean,
val locationServicesEnabled: Boolean,
)
@@ -3,7 +3,5 @@ package com.zaneschepke.networkmonitor
import kotlinx.coroutines.flow.Flow
interface NetworkMonitor {
val networkStatusFlow: Flow<NetworkStatus>
fun sendLocationPermissionsGrantedBroadcast()
val connectivityStateFlow: Flow<ConnectivityState>
}
@@ -1,21 +0,0 @@
package com.zaneschepke.networkmonitor
sealed class NetworkStatus {
data object Disconnected : NetworkStatus() {
override val wifiConnected = false
override val ethernetConnected = false
override val cellularConnected = false
}
data class Connected(
val wifiSsid: String? = null,
val securityType: WifiSecurityType? = null,
override val wifiConnected: Boolean = false,
override val ethernetConnected: Boolean = false,
override val cellularConnected: Boolean = false,
) : NetworkStatus()
abstract val wifiConnected: Boolean
abstract val ethernetConnected: Boolean
abstract val cellularConnected: Boolean
}
@@ -0,0 +1,34 @@
package com.zaneschepke.networkmonitor
import android.net.Network
import android.net.NetworkCapabilities
sealed class TransportEvent {
data class Available(
val network: Network,
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod? = null,
) : TransportEvent()
data class Lost(val network: Network) : TransportEvent()
data class CapabilitiesChanged(
val network: Network,
val networkCapabilities: NetworkCapabilities,
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod? = null,
) : TransportEvent()
data class LocationPermissionGranted(
val network: Network?,
val networkCapabilities: NetworkCapabilities?,
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod?,
) : TransportEvent()
data class LocationServicesChanged(
val enabled: Boolean,
val network: Network?,
val networkCapabilities: NetworkCapabilities?,
val wifiDetectionMethod: AndroidNetworkMonitor.WifiDetectionMethod?,
) : TransportEvent()
data object Unknown : TransportEvent()
}
@@ -1,11 +1,15 @@
package com.zaneschepke.networkmonitor
package com.zaneschepke.networkmonitor.util
import android.location.LocationManager
import android.net.NetworkCapabilities
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.os.Build
import com.wireguard.android.util.RootShell
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.Companion.ANDROID_UNKNOWN_SSID
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
const val WIFI_SSID_SHELL_COMMAND =
"dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: \"[^\"]*\"' | cut -d '\"' -f2"
@@ -19,12 +23,25 @@ fun RootShell.getCurrentWifiName(): String {
@Suppress("DEPRECATION")
fun WifiManager.getCurrentSecurityType(): WifiSecurityType? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
WifiSecurityType.from(connectionInfo.currentSecurityType)
WifiSecurityType.Companion.from(connectionInfo.currentSecurityType)
} else {
null
}
}
@Suppress("DEPRECATION")
suspend fun WifiManager?.getWifiSsid(): String {
return withContext(Dispatchers.IO) {
try {
this@getWifiSsid?.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
?: ANDROID_UNKNOWN_SSID
} catch (e: Exception) {
Timber.e(e)
ANDROID_UNKNOWN_SSID
}
}
}
fun NetworkCapabilities.getWifiSsid(): String {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val info: WifiInfo
@@ -35,3 +52,14 @@ fun NetworkCapabilities.getWifiSsid(): String {
}
return ANDROID_UNKNOWN_SSID
}
fun LocationManager.isLocationServicesEnabled(): Boolean {
return try {
val isGpsEnabled = isProviderEnabled(LocationManager.GPS_PROVIDER)
val isNetworkEnabled = isProviderEnabled(LocationManager.NETWORK_PROVIDER)
isGpsEnabled || isNetworkEnabled
} catch (e: Exception) {
Timber.e(e, "Error checking location services")
false
}
}
@@ -1,4 +1,4 @@
package com.zaneschepke.networkmonitor
package com.zaneschepke.networkmonitor.util
import android.net.wifi.WifiInfo