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)
|
||||
}
|
||||
|
||||
if (Settings.allowBypass) {
|
||||
builder.allowBypass()
|
||||
}
|
||||
|
||||
val inet4Address = options.inet4Address
|
||||
while (inet4Address.hasNext()) {
|
||||
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.padding
|
||||
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.material.icons.Icons
|
||||
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.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -35,18 +40,28 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
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.platform.LocalContext
|
||||
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.style.TextDecoration
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.bg.ServiceConnection
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.ktx.launchCustomTab
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -66,14 +81,13 @@ fun ServiceSettingsScreen(navController: NavController, serviceConnection: Servi
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
// Check battery optimization status
|
||||
val scope = rememberCoroutineScope()
|
||||
var isBatteryOptimizationIgnored by remember { mutableStateOf(false) }
|
||||
// Activity result launcher for battery optimization permission
|
||||
var allowBypass by remember { mutableStateOf(Settings.allowBypass) }
|
||||
val requestBatteryOptimizationLauncher =
|
||||
rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
) { _ ->
|
||||
// Recheck the status after returning from settings
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val pm = context.getSystemService(PowerManager::class.java)
|
||||
isBatteryOptimizationIgnored =
|
||||
@@ -81,7 +95,6 @@ fun ServiceSettingsScreen(navController: NavController, serviceConnection: Servi
|
||||
}
|
||||
}
|
||||
|
||||
// Check battery optimization status on launch
|
||||
LaunchedEffect(Unit) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val pm = context.getSystemService(PowerManager::class.java)
|
||||
@@ -100,7 +113,6 @@ fun ServiceSettingsScreen(navController: NavController, serviceConnection: Servi
|
||||
.verticalScroll(rememberScrollState())
|
||||
.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) {
|
||||
Card(
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -37,10 +35,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -70,15 +65,8 @@ fun SettingsScreen(navController: NavController) {
|
||||
val hookStatus by HookStatusClient.status.collectAsState()
|
||||
val hasPendingPrivilegeDowngrade = HookModuleUpdateNotifier.isDowngrade(hookStatus)
|
||||
val hasPendingPrivilegeUpdate = HookModuleUpdateNotifier.isUpgrade(hookStatus)
|
||||
var isBatteryOptimizationIgnored by remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
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(
|
||||
@@ -153,31 +141,26 @@ fun SettingsScreen(navController: NavController) {
|
||||
),
|
||||
)
|
||||
|
||||
if (!isBatteryOptimizationIgnored) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.service),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Tune,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Badge(containerColor = MaterialTheme.colorScheme.primary)
|
||||
},
|
||||
modifier = Modifier.clickable { navController.navigate("settings/service") },
|
||||
colors =
|
||||
ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.service),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Tune,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable { navController.navigate("settings/service") },
|
||||
colors =
|
||||
ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
|
||||
@@ -23,6 +23,7 @@ object SettingsKey {
|
||||
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 ALLOW_BYPASS = "allow_bypass"
|
||||
const val SYSTEM_PROXY_ENABLED = "system_proxy_enabled"
|
||||
|
||||
const val PRIVILEGE_SETTINGS_ENABLED = "hide_settings_enabled"
|
||||
|
||||
@@ -96,6 +96,7 @@ object Settings {
|
||||
perAppProxyList
|
||||
}
|
||||
|
||||
var allowBypass by dataStore.boolean(SettingsKey.ALLOW_BYPASS) { false }
|
||||
var systemProxyEnabled by dataStore.boolean(SettingsKey.SYSTEM_PROXY_ENABLED) { true }
|
||||
|
||||
var privilegeSettingsEnabled by dataStore.boolean(SettingsKey.PRIVILEGE_SETTINGS_ENABLED) { false }
|
||||
|
||||
@@ -204,6 +204,9 @@
|
||||
<string name="dynamic_notification">نمایش سرعت بلادرنگ در اعلان</string>
|
||||
<string name="disable_notification_description">به دلیل محدودیتهای اندروید، ابتدا باید مجوز اعلان را بدهید، سپس دستهبندی اعلان را در تنظیمات غیرفعال کنید.</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_description">نیازمند دسترسی ROOT</string>
|
||||
<string name="system_http_proxy">پراکسی HTTP سیستم</string>
|
||||
|
||||
@@ -204,6 +204,9 @@
|
||||
<string name="dynamic_notification">Отображать скорость в реальном времени в уведомлении</string>
|
||||
<string name="disable_notification_description">Из-за ограничений 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_description">Требуются права ROOT</string>
|
||||
<string name="system_http_proxy">Системный HTTP-прокси</string>
|
||||
|
||||
@@ -206,6 +206,9 @@
|
||||
<string name="dynamic_notification">在通知中显示实时网速</string>
|
||||
<string name="disable_notification_description">由于 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_description">需要 ROOT 权限</string>
|
||||
<string name="system_http_proxy">系统 HTTP 代理</string>
|
||||
|
||||
@@ -206,6 +206,9 @@
|
||||
<string name="dynamic_notification">在通知中顯示即時網速</string>
|
||||
<string name="disable_notification_description">由於 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_description">需要 ROOT 權限</string>
|
||||
<string name="system_http_proxy">系統 HTTP 代理</string>
|
||||
|
||||
@@ -206,6 +206,9 @@
|
||||
<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_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_description">ROOT permission required</string>
|
||||
<string name="system_http_proxy">System HTTP Proxy</string>
|
||||
|
||||
Reference in New Issue
Block a user