11package com.github.kr328.clash.settings.ui
22
3+ import android.content.Context
4+ import android.content.Intent
5+ import android.net.Uri
6+ import android.net.VpnService
7+ import android.os.PowerManager
8+ import android.provider.Settings
9+ import androidx.activity.compose.rememberLauncherForActivityResult
10+ import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
311import androidx.annotation.StringRes
412import androidx.compose.foundation.layout.fillMaxSize
513import androidx.compose.foundation.lazy.LazyColumn
614import androidx.compose.material3.Icon
715import androidx.compose.material3.Text
816import androidx.compose.runtime.Composable
17+ import androidx.compose.runtime.DisposableEffect
918import androidx.compose.runtime.getValue
19+ import androidx.compose.runtime.mutableStateOf
20+ import androidx.compose.runtime.remember
21+ import androidx.compose.runtime.setValue
1022import androidx.compose.ui.Modifier
23+ import androidx.compose.ui.platform.LocalContext
1124import androidx.compose.ui.res.stringResource
1225import androidx.compose.ui.tooling.preview.PreviewWrapper
26+ import androidx.lifecycle.Lifecycle
27+ import androidx.lifecycle.LifecycleEventObserver
28+ import androidx.lifecycle.compose.LocalLifecycleOwner
1329import androidx.lifecycle.compose.collectAsStateWithLifecycle
1430import androidx.lifecycle.viewmodel.compose.viewModel
1531import com.github.kr328.clash.glue.model.DarkMode
1632import com.github.kr328.clash.settings.R
1733import com.github.kr328.clash.settings.vm.AppSettingsViewModel
1834import com.github.kr328.clash.ui.component.TabbyScaffold
35+ import com.github.kr328.clash.ui.icon.BaselineBatterySaver
1936import com.github.kr328.clash.ui.icon.BaselineBrightness4
2037import com.github.kr328.clash.ui.icon.BaselineDomain
2138import com.github.kr328.clash.ui.icon.BaselineHide
2239import com.github.kr328.clash.ui.icon.BaselineRestore
2340import com.github.kr328.clash.ui.icon.BaselineStack
41+ import com.github.kr328.clash.ui.icon.BaselineVpnLock
2442import com.github.kr328.clash.ui.icon.TabbyIcons
2543import com.github.kr328.clash.ui.theme.PreviewTabby
2644import com.github.kr328.clash.ui.theme.TabbyThemeWrapper
@@ -34,17 +52,63 @@ internal fun AppSettingsScreen(
3452 modifier : Modifier = Modifier ,
3553 viewModel : AppSettingsViewModel = viewModel(),
3654) {
55+ val context = LocalContext .current
56+ val lifecycleOwner = LocalLifecycleOwner .current
3757 val clashRunning by viewModel.clashRunning.collectAsStateWithLifecycle()
3858 val uiState by viewModel.uiState.collectAsStateWithLifecycle()
59+ var vpnPermissionGranted by remember(context) { mutableStateOf(isVpnPermissionGranted(context)) }
60+ var batteryOptimizationIgnored by
61+ remember(context) { mutableStateOf(isBatteryOptimizationIgnored(context)) }
62+
63+ val vpnPermissionLauncher =
64+ rememberLauncherForActivityResult(StartActivityForResult ()) {
65+ vpnPermissionGranted = isVpnPermissionGranted(context)
66+ }
67+ val batteryOptimizationLauncher =
68+ rememberLauncherForActivityResult(StartActivityForResult ()) {
69+ batteryOptimizationIgnored = isBatteryOptimizationIgnored(context)
70+ }
71+
72+ DisposableEffect (context, lifecycleOwner) {
73+ val observer = LifecycleEventObserver { _, event ->
74+ if (event == Lifecycle .Event .ON_RESUME ) {
75+ vpnPermissionGranted = isVpnPermissionGranted(context)
76+ batteryOptimizationIgnored = isBatteryOptimizationIgnored(context)
77+ }
78+ }
79+ lifecycleOwner.lifecycle.addObserver(observer)
80+
81+ onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
82+ }
3983
4084 AppSettingsContent (
4185 clashRunning = clashRunning,
4286 uiState = uiState,
87+ vpnPermissionGranted = vpnPermissionGranted,
88+ batteryOptimizationIgnored = batteryOptimizationIgnored,
4389 onAutoRestartChange = viewModel::updateAutoRestart,
4490 onDarkModeChange = viewModel::updateDarkMode,
4591 onHideAppIconChange = viewModel::updateHideAppIcon,
4692 onHideFromRecentsChange = viewModel::updateHideFromRecents,
4793 onDynamicNotificationChange = viewModel::updateDynamicNotification,
94+ onAlwaysOnVpnChange = { enabled ->
95+ if (! enabled) return @AppSettingsContent
96+
97+ val permissionIntent = VpnService .prepare(context)
98+ if (permissionIntent != null ) {
99+ vpnPermissionLauncher.launch(permissionIntent)
100+ } else {
101+ context.startActivity(Intent (Settings .ACTION_VPN_SETTINGS ))
102+ }
103+ },
104+ onIgnoreBatteryOptimizationChange = { enabled ->
105+ if (! enabled) return @AppSettingsContent
106+
107+ batteryOptimizationLauncher.launch(
108+ Intent (Settings .ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS )
109+ .setData(Uri .parse(" package:${context.packageName} " ))
110+ )
111+ },
48112 modifier = modifier,
49113 )
50114}
@@ -53,11 +117,15 @@ internal fun AppSettingsScreen(
53117private fun AppSettingsContent (
54118 clashRunning : Boolean ,
55119 uiState : AppSettingsViewModel .UiState ,
120+ vpnPermissionGranted : Boolean ,
121+ batteryOptimizationIgnored : Boolean ,
56122 onAutoRestartChange : (Boolean ) -> Unit ,
57123 onDarkModeChange : (DarkMode ) -> Unit ,
58124 onHideAppIconChange : (Boolean ) -> Unit ,
59125 onHideFromRecentsChange : (Boolean ) -> Unit ,
60126 onDynamicNotificationChange : (Boolean ) -> Unit ,
127+ onAlwaysOnVpnChange : (Boolean ) -> Unit ,
128+ onIgnoreBatteryOptimizationChange : (Boolean ) -> Unit ,
61129 modifier : Modifier = Modifier ,
62130) {
63131 TabbyScaffold (title = stringResource(R .string.app), modifier = modifier) { innerPadding ->
@@ -115,6 +183,24 @@ private fun AppSettingsContent(
115183 title = { Text (stringResource(R .string.show_traffic)) },
116184 summary = { Text (stringResource(R .string.show_traffic_summary)) },
117185 )
186+ switchPreference(
187+ key = " always_on_vpn" ,
188+ value = vpnPermissionGranted,
189+ onValueChange = onAlwaysOnVpnChange,
190+ enabled = ! vpnPermissionGranted,
191+ icon = { Icon (imageVector = TabbyIcons .BaselineVpnLock , contentDescription = null ) },
192+ title = { Text (stringResource(R .string.always_on_vpn)) },
193+ summary = { Text (stringResource(R .string.always_on_vpn_summary)) },
194+ )
195+ switchPreference(
196+ key = " ignore_battery_optimizations" ,
197+ value = batteryOptimizationIgnored,
198+ onValueChange = onIgnoreBatteryOptimizationChange,
199+ enabled = ! batteryOptimizationIgnored,
200+ icon = { Icon (imageVector = TabbyIcons .BaselineBatterySaver , contentDescription = null ) },
201+ title = { Text (stringResource(R .string.ignore_battery_optimizations)) },
202+ summary = { Text (stringResource(R .string.ignore_battery_optimizations_summary)) },
203+ )
118204 }
119205 }
120206 }
@@ -143,11 +229,15 @@ private fun AppSettingsScreenPreview() {
143229 hideFromRecents = false ,
144230 dynamicNotification = true ,
145231 ),
232+ vpnPermissionGranted = false ,
233+ batteryOptimizationIgnored = false ,
146234 onAutoRestartChange = {},
147235 onDarkModeChange = {},
148236 onHideAppIconChange = {},
149237 onHideFromRecentsChange = {},
150238 onDynamicNotificationChange = {},
239+ onAlwaysOnVpnChange = {},
240+ onIgnoreBatteryOptimizationChange = {},
151241 )
152242}
153243
@@ -165,10 +255,24 @@ private fun AppSettingsScreenRunningPreview() {
165255 hideFromRecents = true ,
166256 dynamicNotification = true ,
167257 ),
258+ vpnPermissionGranted = true ,
259+ batteryOptimizationIgnored = true ,
168260 onAutoRestartChange = {},
169261 onDarkModeChange = {},
170262 onHideAppIconChange = {},
171263 onHideFromRecentsChange = {},
172264 onDynamicNotificationChange = {},
265+ onAlwaysOnVpnChange = {},
266+ onIgnoreBatteryOptimizationChange = {},
173267 )
174268}
269+
270+ private fun isVpnPermissionGranted (context : Context ): Boolean {
271+ return VpnService .prepare(context) == null
272+ }
273+
274+ private fun isBatteryOptimizationIgnored (context : Context ): Boolean {
275+ return context
276+ .getSystemService(PowerManager ::class .java)
277+ ?.isIgnoringBatteryOptimizations(context.packageName) == true
278+ }
0 commit comments