Add Allow Bypass VPN setting to service screen
Add a toggle in the service settings screen that calls VpnService.Builder.allowBypass() when enabled, with a description linking to Android documentation. Always show the Service item in the settings list (remove battery-optimization-gated visibility).
This commit is contained in:
@@ -66,6 +66,10 @@ class VPNService :
|
|||||||
builder.setMetered(false)
|
builder.setMetered(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Settings.allowBypass) {
|
||||||
|
builder.allowBypass()
|
||||||
|
}
|
||||||
|
|
||||||
val inet4Address = options.inet4Address
|
val inet4Address = options.inet4Address
|
||||||
while (inet4Address.hasNext()) {
|
while (inet4Address.hasNext()) {
|
||||||
val address = inet4Address.next()
|
val address = inet4Address.next()
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.ClickableText
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
@@ -26,8 +28,11 @@ import androidx.compose.material3.CardDefaults
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -35,18 +40,28 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.bg.ServiceConnection
|
import io.nekohasekai.sfa.bg.ServiceConnection
|
||||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||||
|
import io.nekohasekai.sfa.database.Settings
|
||||||
import io.nekohasekai.sfa.ktx.launchCustomTab
|
import io.nekohasekai.sfa.ktx.launchCustomTab
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -66,14 +81,13 @@ fun ServiceSettingsScreen(navController: NavController, serviceConnection: Servi
|
|||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
// Check battery optimization status
|
val scope = rememberCoroutineScope()
|
||||||
var isBatteryOptimizationIgnored by remember { mutableStateOf(false) }
|
var isBatteryOptimizationIgnored by remember { mutableStateOf(false) }
|
||||||
// Activity result launcher for battery optimization permission
|
var allowBypass by remember { mutableStateOf(Settings.allowBypass) }
|
||||||
val requestBatteryOptimizationLauncher =
|
val requestBatteryOptimizationLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.StartActivityForResult(),
|
ActivityResultContracts.StartActivityForResult(),
|
||||||
) { _ ->
|
) { _ ->
|
||||||
// Recheck the status after returning from settings
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
val pm = context.getSystemService(PowerManager::class.java)
|
val pm = context.getSystemService(PowerManager::class.java)
|
||||||
isBatteryOptimizationIgnored =
|
isBatteryOptimizationIgnored =
|
||||||
@@ -81,7 +95,6 @@ fun ServiceSettingsScreen(navController: NavController, serviceConnection: Servi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check battery optimization status on launch
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
val pm = context.getSystemService(PowerManager::class.java)
|
val pm = context.getSystemService(PowerManager::class.java)
|
||||||
@@ -100,7 +113,6 @@ fun ServiceSettingsScreen(navController: NavController, serviceConnection: Servi
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(vertical = 8.dp),
|
.padding(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
// Background Permission Card (only show if battery optimization is not ignored)
|
|
||||||
if (!isBatteryOptimizationIgnored && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (!isBatteryOptimizationIgnored && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
Card(
|
Card(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -171,6 +183,93 @@ fun ServiceSettingsScreen(navController: NavController, serviceConnection: Servi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VPN Section
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "VPN",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val descriptionText = stringResource(R.string.allow_bypass_description)
|
||||||
|
val linkText = stringResource(R.string.android_documentation)
|
||||||
|
val linkColor = MaterialTheme.colorScheme.primary
|
||||||
|
val textColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
val textStyle = MaterialTheme.typography.bodyMedium
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.allow_bypass),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
val annotatedString = buildAnnotatedString {
|
||||||
|
withStyle(SpanStyle(color = textColor)) {
|
||||||
|
append(descriptionText)
|
||||||
|
}
|
||||||
|
append("\n\n")
|
||||||
|
pushStringAnnotation(tag = "URL", annotation = ALLOW_BYPASS_DOC_URL)
|
||||||
|
withStyle(
|
||||||
|
SpanStyle(
|
||||||
|
color = linkColor,
|
||||||
|
textDecoration = TextDecoration.Underline,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
append(linkText)
|
||||||
|
}
|
||||||
|
pop()
|
||||||
|
}
|
||||||
|
ClickableText(
|
||||||
|
text = annotatedString,
|
||||||
|
style = textStyle,
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
onClick = { offset ->
|
||||||
|
annotatedString.getStringAnnotations(
|
||||||
|
tag = "URL",
|
||||||
|
start = offset,
|
||||||
|
end = offset,
|
||||||
|
).firstOrNull()?.let {
|
||||||
|
context.launchCustomTab(it.item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = allowBypass,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
allowBypass = checked
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
Settings.allowBypass = checked
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clip(RoundedCornerShape(12.dp)),
|
||||||
|
colors =
|
||||||
|
ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val ALLOW_BYPASS_DOC_URL =
|
||||||
|
"https://developer.android.com/reference/android/net/VpnService.Builder#allowBypass()"
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package io.nekohasekai.sfa.compose.screen.settings
|
package io.nekohasekai.sfa.compose.screen.settings
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.PowerManager
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -37,10 +35,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -70,15 +65,8 @@ fun SettingsScreen(navController: NavController) {
|
|||||||
val hookStatus by HookStatusClient.status.collectAsState()
|
val hookStatus by HookStatusClient.status.collectAsState()
|
||||||
val hasPendingPrivilegeDowngrade = HookModuleUpdateNotifier.isDowngrade(hookStatus)
|
val hasPendingPrivilegeDowngrade = HookModuleUpdateNotifier.isDowngrade(hookStatus)
|
||||||
val hasPendingPrivilegeUpdate = HookModuleUpdateNotifier.isUpgrade(hookStatus)
|
val hasPendingPrivilegeUpdate = HookModuleUpdateNotifier.isUpgrade(hookStatus)
|
||||||
var isBatteryOptimizationIgnored by remember { mutableStateOf(true) }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
HookStatusClient.refresh()
|
HookStatusClient.refresh()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
val pm = context.getSystemService(PowerManager::class.java)
|
|
||||||
isBatteryOptimizationIgnored =
|
|
||||||
pm?.isIgnoringBatteryOptimizations(context.packageName) == true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -153,31 +141,26 @@ fun SettingsScreen(navController: NavController) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!isBatteryOptimizationIgnored) {
|
ListItem(
|
||||||
ListItem(
|
headlineContent = {
|
||||||
headlineContent = {
|
Text(
|
||||||
Text(
|
stringResource(R.string.service),
|
||||||
stringResource(R.string.service),
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
)
|
||||||
)
|
},
|
||||||
},
|
leadingContent = {
|
||||||
leadingContent = {
|
Icon(
|
||||||
Icon(
|
imageVector = Icons.Outlined.Tune,
|
||||||
imageVector = Icons.Outlined.Tune,
|
contentDescription = null,
|
||||||
contentDescription = null,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
)
|
||||||
)
|
},
|
||||||
},
|
modifier = Modifier.clickable { navController.navigate("settings/service") },
|
||||||
trailingContent = {
|
colors =
|
||||||
Badge(containerColor = MaterialTheme.colorScheme.primary)
|
ListItemDefaults.colors(
|
||||||
},
|
containerColor = Color.Transparent,
|
||||||
modifier = Modifier.clickable { navController.navigate("settings/service") },
|
),
|
||||||
colors =
|
)
|
||||||
ListItemDefaults.colors(
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ object SettingsKey {
|
|||||||
const val PER_APP_PROXY_MANAGED_LIST = "per_app_proxy_managed_list"
|
const val PER_APP_PROXY_MANAGED_LIST = "per_app_proxy_managed_list"
|
||||||
const val PER_APP_PROXY_PACKAGE_QUERY_MODE = "per_app_proxy_package_query_mode"
|
const val PER_APP_PROXY_PACKAGE_QUERY_MODE = "per_app_proxy_package_query_mode"
|
||||||
|
|
||||||
|
const val ALLOW_BYPASS = "allow_bypass"
|
||||||
const val SYSTEM_PROXY_ENABLED = "system_proxy_enabled"
|
const val SYSTEM_PROXY_ENABLED = "system_proxy_enabled"
|
||||||
|
|
||||||
const val PRIVILEGE_SETTINGS_ENABLED = "hide_settings_enabled"
|
const val PRIVILEGE_SETTINGS_ENABLED = "hide_settings_enabled"
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ object Settings {
|
|||||||
perAppProxyList
|
perAppProxyList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var allowBypass by dataStore.boolean(SettingsKey.ALLOW_BYPASS) { false }
|
||||||
var systemProxyEnabled by dataStore.boolean(SettingsKey.SYSTEM_PROXY_ENABLED) { true }
|
var systemProxyEnabled by dataStore.boolean(SettingsKey.SYSTEM_PROXY_ENABLED) { true }
|
||||||
|
|
||||||
var privilegeSettingsEnabled by dataStore.boolean(SettingsKey.PRIVILEGE_SETTINGS_ENABLED) { false }
|
var privilegeSettingsEnabled by dataStore.boolean(SettingsKey.PRIVILEGE_SETTINGS_ENABLED) { false }
|
||||||
|
|||||||
@@ -204,6 +204,9 @@
|
|||||||
<string name="dynamic_notification">نمایش سرعت بلادرنگ در اعلان</string>
|
<string name="dynamic_notification">نمایش سرعت بلادرنگ در اعلان</string>
|
||||||
<string name="disable_notification_description">به دلیل محدودیتهای اندروید، ابتدا باید مجوز اعلان را بدهید، سپس دستهبندی اعلان را در تنظیمات غیرفعال کنید.</string>
|
<string name="disable_notification_description">به دلیل محدودیتهای اندروید، ابتدا باید مجوز اعلان را بدهید، سپس دستهبندی اعلان را در تنظیمات غیرفعال کنید.</string>
|
||||||
<string name="disable_notification_description_legacy">به دلیل محدودیتهای اندروید، ابتدا باید مجوز اعلان را بدهید، سپس اعلانها را در اطلاعات برنامه غیرفعال کنید.</string>
|
<string name="disable_notification_description_legacy">به دلیل محدودیتهای اندروید، ابتدا باید مجوز اعلان را بدهید، سپس اعلانها را در اطلاعات برنامه غیرفعال کنید.</string>
|
||||||
|
<string name="allow_bypass">اجازه دور زدن VPN</string>
|
||||||
|
<string name="allow_bypass_description">در صورت فعال بودن، برنامهها میتوانند این اتصال VPN را دور بزنند و مستقیماً از شبکه اصلی استفاده کنند.</string>
|
||||||
|
<string name="android_documentation">مستندات Android</string>
|
||||||
<string name="auto_redirect">تغییر مسیر خودکار</string>
|
<string name="auto_redirect">تغییر مسیر خودکار</string>
|
||||||
<string name="auto_redirect_description">نیازمند دسترسی ROOT</string>
|
<string name="auto_redirect_description">نیازمند دسترسی ROOT</string>
|
||||||
<string name="system_http_proxy">پراکسی HTTP سیستم</string>
|
<string name="system_http_proxy">پراکسی HTTP سیستم</string>
|
||||||
|
|||||||
@@ -204,6 +204,9 @@
|
|||||||
<string name="dynamic_notification">Отображать скорость в реальном времени в уведомлении</string>
|
<string name="dynamic_notification">Отображать скорость в реальном времени в уведомлении</string>
|
||||||
<string name="disable_notification_description">Из-за ограничений Android необходимо сначала предоставить разрешение на уведомления, а затем отключить категорию уведомлений в настройках.</string>
|
<string name="disable_notification_description">Из-за ограничений Android необходимо сначала предоставить разрешение на уведомления, а затем отключить категорию уведомлений в настройках.</string>
|
||||||
<string name="disable_notification_description_legacy">Из-за ограничений Android необходимо сначала предоставить разрешение на уведомления, а затем отключить уведомления в сведениях о приложении.</string>
|
<string name="disable_notification_description_legacy">Из-за ограничений Android необходимо сначала предоставить разрешение на уведомления, а затем отключить уведомления в сведениях о приложении.</string>
|
||||||
|
<string name="allow_bypass">Разрешить обход VPN</string>
|
||||||
|
<string name="allow_bypass_description">Если включено, приложения могут обойти это VPN-соединение и использовать базовую сеть напрямую.</string>
|
||||||
|
<string name="android_documentation">Документация Android</string>
|
||||||
<string name="auto_redirect">Автоматическое перенаправление</string>
|
<string name="auto_redirect">Автоматическое перенаправление</string>
|
||||||
<string name="auto_redirect_description">Требуются права ROOT</string>
|
<string name="auto_redirect_description">Требуются права ROOT</string>
|
||||||
<string name="system_http_proxy">Системный HTTP-прокси</string>
|
<string name="system_http_proxy">Системный HTTP-прокси</string>
|
||||||
|
|||||||
@@ -206,6 +206,9 @@
|
|||||||
<string name="dynamic_notification">在通知中显示实时网速</string>
|
<string name="dynamic_notification">在通知中显示实时网速</string>
|
||||||
<string name="disable_notification_description">由于 Android 限制,您需要先授权通知权限,然后前往系统设置中关闭通知类别。</string>
|
<string name="disable_notification_description">由于 Android 限制,您需要先授权通知权限,然后前往系统设置中关闭通知类别。</string>
|
||||||
<string name="disable_notification_description_legacy">由于 Android 限制,您需要先授权通知权限,然后前往应用信息中关闭通知。</string>
|
<string name="disable_notification_description_legacy">由于 Android 限制,您需要先授权通知权限,然后前往应用信息中关闭通知。</string>
|
||||||
|
<string name="allow_bypass">允许绕过 VPN</string>
|
||||||
|
<string name="allow_bypass_description">启用后,应用可以绕过此 VPN 连接,直接使用底层网络。</string>
|
||||||
|
<string name="android_documentation">Android 文档</string>
|
||||||
<string name="auto_redirect">自动重定向</string>
|
<string name="auto_redirect">自动重定向</string>
|
||||||
<string name="auto_redirect_description">需要 ROOT 权限</string>
|
<string name="auto_redirect_description">需要 ROOT 权限</string>
|
||||||
<string name="system_http_proxy">系统 HTTP 代理</string>
|
<string name="system_http_proxy">系统 HTTP 代理</string>
|
||||||
|
|||||||
@@ -206,6 +206,9 @@
|
|||||||
<string name="dynamic_notification">在通知中顯示即時網速</string>
|
<string name="dynamic_notification">在通知中顯示即時網速</string>
|
||||||
<string name="disable_notification_description">由於 Android 限制,您需要先授權通知權限,然後前往系統設定中關閉通知類別。</string>
|
<string name="disable_notification_description">由於 Android 限制,您需要先授權通知權限,然後前往系統設定中關閉通知類別。</string>
|
||||||
<string name="disable_notification_description_legacy">由於 Android 限制,您需要先授權通知權限,然後前往應用程式資訊中關閉通知。</string>
|
<string name="disable_notification_description_legacy">由於 Android 限制,您需要先授權通知權限,然後前往應用程式資訊中關閉通知。</string>
|
||||||
|
<string name="allow_bypass">允許繞過 VPN</string>
|
||||||
|
<string name="allow_bypass_description">啟用後,應用程式可以繞過此 VPN 連線,直接使用底層網路。</string>
|
||||||
|
<string name="android_documentation">Android 文件</string>
|
||||||
<string name="auto_redirect">自動重定向</string>
|
<string name="auto_redirect">自動重定向</string>
|
||||||
<string name="auto_redirect_description">需要 ROOT 權限</string>
|
<string name="auto_redirect_description">需要 ROOT 權限</string>
|
||||||
<string name="system_http_proxy">系統 HTTP 代理</string>
|
<string name="system_http_proxy">系統 HTTP 代理</string>
|
||||||
|
|||||||
@@ -206,6 +206,9 @@
|
|||||||
<string name="dynamic_notification">Display realtime speed in notification</string>
|
<string name="dynamic_notification">Display realtime speed in notification</string>
|
||||||
<string name="disable_notification_description">Due to Android restrictions, you must first grant notification permission, then go to Settings to disable the notification category.</string>
|
<string name="disable_notification_description">Due to Android restrictions, you must first grant notification permission, then go to Settings to disable the notification category.</string>
|
||||||
<string name="disable_notification_description_legacy">Due to Android restrictions, you must first grant notification permission, then go to App Info to disable notifications.</string>
|
<string name="disable_notification_description_legacy">Due to Android restrictions, you must first grant notification permission, then go to App Info to disable notifications.</string>
|
||||||
|
<string name="allow_bypass">Allow Bypass</string>
|
||||||
|
<string name="allow_bypass_description">If enabled, applications can bypass this VPN connection and instead use the underlying network directly.</string>
|
||||||
|
<string name="android_documentation">Android Documentation</string>
|
||||||
<string name="auto_redirect">Auto Redirect</string>
|
<string name="auto_redirect">Auto Redirect</string>
|
||||||
<string name="auto_redirect_description">ROOT permission required</string>
|
<string name="auto_redirect_description">ROOT permission required</string>
|
||||||
<string name="system_http_proxy">System HTTP Proxy</string>
|
<string name="system_http_proxy">System HTTP Proxy</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user