diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/settings/SettingsActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/settings/SettingsActivity.kt index d044521e..b31a19fb 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/settings/SettingsActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/settings/SettingsActivity.kt @@ -1,7 +1,10 @@ package com.v2ray.ang.ui.settings +import android.content.ActivityNotFoundException +import android.content.Intent import android.os.Build import android.os.Bundle +import android.provider.Settings import androidx.activity.viewModels import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -24,10 +27,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig.VPN import com.v2ray.ang.R +import com.v2ray.ang.extension.toastError import com.v2ray.ang.handler.AppLocaleManager import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager.rememberMmkvBool @@ -44,7 +51,9 @@ import com.v2ray.ang.ui.compose.SettingsMenuItem import com.v2ray.ang.ui.compose.SettingsSwitchItem import com.v2ray.ang.ui.compose.ThemeManager import com.v2ray.ang.ui.compose.verticalScrollbar +import com.v2ray.ang.util.LogUtil import com.v2ray.ang.util.Utils +import kotlinx.coroutines.launch class SettingsActivity : BaseComponentActivity() { @@ -52,6 +61,26 @@ class SettingsActivity : BaseComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel.refreshSystemVpnSettingsAvailability() + } + } + } + + private fun openSystemVpnSettings() { + try { + startActivity(Intent(Settings.ACTION_VPN_SETTINGS)) + } catch (error: ActivityNotFoundException) { + reportSystemVpnSettingsFailure(error) + } catch (error: SecurityException) { + reportSystemVpnSettingsFailure(error) + } + } + + private fun reportSystemVpnSettingsFailure(error: RuntimeException) { + LogUtil.e(AppConfig.TAG, "Cannot open system VPN settings", error) + toastError(R.string.toast_system_vpn_settings_unavailable) } @Composable @@ -59,7 +88,8 @@ class SettingsActivity : BaseComponentActivity() { SettingsScreen( viewModel = viewModel, onBackClick = { finish() }, - onModeHelpClicked = { Utils.openUri(this, AppConfig.APP_WIKI_MODE) } + onModeHelpClicked = { Utils.openUri(this, AppConfig.APP_WIKI_MODE) }, + onSystemVpnSettingsClicked = ::openSystemVpnSettings ) } } @@ -69,10 +99,12 @@ class SettingsActivity : BaseComponentActivity() { fun SettingsScreen( viewModel: SettingsViewModel, onBackClick: () -> Unit, - onModeHelpClicked: () -> Unit + onModeHelpClicked: () -> Unit, + onSystemVpnSettingsClicked: () -> Unit ) { val scrollState = rememberScrollState() val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() + val systemVpnSettingsAvailable by viewModel.systemVpnSettingsAvailable.collectAsStateWithLifecycle() var uiSettingsExpanded by rememberSaveable { mutableStateOf(true) } var vpnSettingsExpanded by rememberSaveable { mutableStateOf(true) } var coreSettingsExpanded by rememberSaveable { mutableStateOf(true) } @@ -612,6 +644,13 @@ fun SettingsScreen( checked = isBooted, onCheckedChange = { isBooted = it } ) + if (systemVpnSettingsAvailable) { + SettingsMenuItem( + title = stringResource(R.string.title_system_vpn_settings), + subtitle = stringResource(R.string.summary_system_vpn_settings), + onClick = onSystemVpnSettingsClicked + ) + } SettingsEditItem( title = stringResource(R.string.title_pref_delay_test_url), value = delayTestUrl, diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/settings/SettingsViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/settings/SettingsViewModel.kt index 83d44309..b2852350 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/settings/SettingsViewModel.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/settings/SettingsViewModel.kt @@ -1,15 +1,29 @@ package com.v2ray.ang.ui.settings import android.app.Application +import android.content.Intent +import android.provider.Settings import com.v2ray.ang.AppConfig import com.v2ray.ang.R import com.v2ray.ang.root.RootManager import com.v2ray.ang.ui.base.BaseViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.withContext class SettingsViewModel(application: Application) : BaseViewModel(application) { + private val _systemVpnSettingsAvailable = MutableStateFlow(false) + val systemVpnSettingsAvailable = _systemVpnSettingsAvailable.asStateFlow() + + suspend fun refreshSystemVpnSettingsAvailability() { + _systemVpnSettingsAvailable.value = withContext(Dispatchers.IO) { + // Android exposes the VPN page, not a direct link to the Always-on VPN switch. + Intent(Settings.ACTION_VPN_SETTINGS).resolveActivity(getApplication().packageManager) != null + } + } + /** * Checks for root access and requests it if necessary. * Updates [isLoading] during the process. @@ -56,4 +70,4 @@ class SettingsViewModel(application: Application) : BaseViewModel(application) { null } } -} \ No newline at end of file +} diff --git a/V2rayNG/app/src/main/res/values-ar/strings.xml b/V2rayNG/app/src/main/res/values-ar/strings.xml index 378b71ae..f4ea53c2 100644 --- a/V2rayNG/app/src/main/res/values-ar/strings.xml +++ b/V2rayNG/app/src/main/res/values-ar/strings.xml @@ -172,8 +172,11 @@ إعدادات VPN الوكيل لكل تطبيق عام: تستخدم التطبيقات المحددة الوكيل، وتتصل التطبيقات غير المحددة مباشرة؛\nوضع التجاوز: تتصل التطبيقات المحددة مباشرة، وتستخدم التطبيقات غير المحددة الوكيل.\nيتوفر في القائمة خيار لتحديد التطبيقات التي ستستخدم الوكيل تلقائيًا. - الاتصال تلقائياً عند بدء التشغيل - الاتصال تلقائياً بالخادم المحدد عند بدء التشغيل؛ قد لا تنجح المحاولة + الاتصال بعد إقلاع الجهاز + الاتصال تلقائياً بالخادم المحدد بعد إقلاع الجهاز. قد يقيّد Android بدء التشغيل في الخلفية. + إعدادات VPN للنظام + فتح إعدادات VPN في Android لإعداد VPN الدائم، إذا كان الجهاز يدعمه. + إعدادات VPN للنظام غير متاحة. ستعمل المهمة في الخلفية، وسيُعرض تقدمها في الإشعار. diff --git a/V2rayNG/app/src/main/res/values-bn/strings.xml b/V2rayNG/app/src/main/res/values-bn/strings.xml index 78effa02..425e5a50 100644 --- a/V2rayNG/app/src/main/res/values-bn/strings.xml +++ b/V2rayNG/app/src/main/res/values-bn/strings.xml @@ -172,8 +172,11 @@ VPN সেটিংস প্রতি-অ্যাপ প্রক্সি সাধারণ: নির্বাচিত অ্যাপ প্রক্সি ব্যবহার করে, অনির্বাচিত অ্যাপ সরাসরি সংযুক্ত হয়;\nবাইপাস মোড: নির্বাচিত অ্যাপ সরাসরি সংযুক্ত হয়, অনির্বাচিত অ্যাপ প্রক্সি ব্যবহার করে।\nযেসব অ্যাপ প্রক্সি ব্যবহার করবে, সেগুলি স্বয়ংক্রিয়ভাবে নির্বাচনের বিকল্পটি মেনুতে আছে। - চালুর সময় স্বয়ংক্রিয় সংযোগ - চালুর সময় নির্বাচিত সার্ভারে স্বয়ংক্রিয়ভাবে সংযোগ করে; সংযোগ ব্যর্থ হতে পারে + ডিভাইস চালু হওয়ার পর সংযোগ + ডিভাইস চালু হওয়ার পর নির্বাচিত সার্ভারে স্বয়ংক্রিয়ভাবে সংযোগ করুন। Android ব্যাকগ্রাউন্ডে চালু হওয়া সীমিত করতে পারে। + সিস্টেমের VPN সেটিংস + ডিভাইসে সমর্থিত হলে সর্বদা চালু VPN কনফিগার করতে Android-এর VPN সেটিংস খুলুন। + সিস্টেমের VPN সেটিংস উপলভ্য নয়। কাজটি ব্যাকগ্রাউন্ডে চলবে এবং অগ্রগতির তথ্য বিজ্ঞপ্তিতে দেখানো হবে। diff --git a/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml b/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml index f85a9418..6c8ebc8d 100644 --- a/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml +++ b/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml @@ -172,8 +172,11 @@ سامووا VPN پروکسی و ری برنومه پوی وولاتی: برنومه پسند وابیڌه وا ی پروکسی منپیز ابۊ، برنومه پسند نوابیڌه موستقیمن منپیز ابۊ.\nحالت دور زیڌن: برنومه پسند وابیڌه موستقیمن منپیز ابۊ، برنومه پسند نوابیڌه وا ی پروکسی منپیز ابۊ.\nپسند خوتکار برنومه یلی ک وا ی پروکسی منپیز ابۊن من نومگه امکووݩ پذیر هڌ. - منپیز خوتکار مجال ره ونی - مجال ره وندن، خوساخوس و سرور پسند بیڌه منپیز ابۊ که گاشڌ نا مووفق بۊ. + منپیز بئڌ ره وندن دسگا + بئڌ ره وندن دسگا، خوساخوس و سرور پسند بیڌه منپیز ابۊ. Android گاشڌ ره وندن من پس زمینه ن مئدۊد کونه. + سامووا VPN سیستوم + سامووا VPN من Android ن وا کۊنین سی ساموو کردن VPN همیشه ره وست، ٱر دسگا زس پشتیبانی اکونه. + سامووا VPN سیستوم من دسرس نین. وزیفه من پس زمینه کار اکونه وو ایلاعات پؽشرفت من اعلان نشووݩ داڌه ابۊ. diff --git a/V2rayNG/app/src/main/res/values-fa/strings.xml b/V2rayNG/app/src/main/res/values-fa/strings.xml index 116846f3..a9e3773a 100644 --- a/V2rayNG/app/src/main/res/values-fa/strings.xml +++ b/V2rayNG/app/src/main/res/values-fa/strings.xml @@ -172,8 +172,11 @@ تنظیمات VPN پروکسی به تفکیک برنامه حالت عادی: برنامه‌های انتخاب‌شده از پروکسی استفاده می‌کنند و برنامه‌های انتخاب‌نشده مستقیماً متصل می‌شوند؛\nحالت دور زدن: برنامه‌های انتخاب‌شده مستقیماً متصل می‌شوند و برنامه‌های انتخاب‌نشده از پروکسی استفاده می‌کنند.\nگزینهٔ انتخاب خودکار برنامه‌هایی که باید از پروکسی استفاده کنند در منو قرار دارد. - اتصال خودکار هنگام راه اندازی - هنگام راه اندازی به طور خودکار به سرور انتخابی متصل می شود که ممکن است ناموفق باشد. + اتصال پس از راه‌اندازی دستگاه + پس از راه‌اندازی دستگاه، به‌طور خودکار به سرور انتخاب‌شده متصل شود. ممکن است Android اجرای پس‌زمینه را محدود کند. + تنظیمات VPN سیستم + تنظیمات VPN در Android را برای پیکربندی VPN همیشه‌روشن باز کنید، اگر دستگاه از آن پشتیبانی می‌کند. + تنظیمات VPN سیستم در دسترس نیست. این کار در پس‌زمینه اجرا می‌شود و پیشرفت آن در اعلان نمایش داده خواهد شد. diff --git a/V2rayNG/app/src/main/res/values-ru/strings.xml b/V2rayNG/app/src/main/res/values-ru/strings.xml index ee93da42..a0ab82c8 100644 --- a/V2rayNG/app/src/main/res/values-ru/strings.xml +++ b/V2rayNG/app/src/main/res/values-ru/strings.xml @@ -172,8 +172,11 @@ Настройки VPN Прокси для выбранных приложений Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nРежим обхода: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nВ меню можно автоматически выбрать приложения, которые должны использовать прокси. - Автоподключение при запуске - Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным) + Подключаться после загрузки устройства + Автоматически подключаться к выбранному серверу после загрузки устройства. Android может ограничивать запуск в фоне. + Системные настройки VPN + Открыть настройки VPN в Android для включения постоянного VPN-подключения, если устройство его поддерживает. + Системные настройки VPN недоступны. Задача будет выполняться в фоновом режиме, информация о ходе выполнения будет отображаться в уведомлении. diff --git a/V2rayNG/app/src/main/res/values-vi/strings.xml b/V2rayNG/app/src/main/res/values-vi/strings.xml index 4e912e42..9019a31c 100644 --- a/V2rayNG/app/src/main/res/values-vi/strings.xml +++ b/V2rayNG/app/src/main/res/values-vi/strings.xml @@ -172,8 +172,11 @@ Cài đặt VPN Proxy theo từng ứng dụng Thông thường: ứng dụng đã chọn dùng proxy, ứng dụng chưa chọn kết nối trực tiếp;\nChế độ bỏ qua: ứng dụng đã chọn kết nối trực tiếp, ứng dụng chưa chọn dùng proxy.\nTrong trình đơn có tùy chọn tự động chọn các ứng dụng sẽ dùng proxy. - Tự động kết nối khi khởi động - Tự động kết nối với máy chủ đã chọn khi khởi động; thao tác này có thể không thành công + Kết nối sau khi thiết bị khởi động + Tự động kết nối với máy chủ đã chọn sau khi thiết bị khởi động. Android có thể hạn chế khởi chạy trong nền. + Cài đặt VPN hệ thống + Mở cài đặt VPN của Android để cấu hình VPN luôn bật, nếu thiết bị này hỗ trợ. + Không có cài đặt VPN hệ thống. Tác vụ sẽ chạy trong nền và thông tin tiến độ sẽ hiển thị trong thông báo. diff --git a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml index 0ceb0b88..db7a47d8 100644 --- a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml +++ b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml @@ -172,8 +172,11 @@ VPN 设置 分应用代理 常规:勾选的应用使用代理,未勾选的应用直连;\n绕行模式:勾选的应用直连,未勾选的应用使用代理。\n菜单中可自动选择需要使用代理的应用。 - 开机时自动连接 - 开机时自动连接选择的服务器,可能会不成功 + 设备启动后自动连接 + 设备启动后自动连接到所选服务器。Android 可能会限制后台启动。 + 系统 VPN 设置 + 打开 Android VPN 设置,配置始终开启的 VPN(如果此设备支持)。 + 系统 VPN 设置不可用。 任务将在后台运行,在通知中会显示进度信息 diff --git a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml index cb7bae55..8dc868d8 100644 --- a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml +++ b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml @@ -172,8 +172,11 @@ VPN 設定 個別應用程式代理 一般:已勾選的應用程式使用代理,未勾選的應用程式直接連線;\n繞行模式:已勾選的應用程式直接連線,未勾選的應用程式使用代理。\n選單中可自動選取需要使用代理的應用程式。 - 開機時自動連線 - 開機時自動連線選擇的伺服器,可能會不成功 + 裝置開機後自動連線 + 裝置開機後自動連線至所選伺服器。Android 可能會限制背景啟動。 + 系統 VPN 設定 + 開啟 Android VPN 設定,設定永遠開啟的 VPN(若此裝置支援)。 + 系統 VPN 設定無法使用。 任務將在背景執行,通知中會顯示進度資訊。 diff --git a/V2rayNG/app/src/main/res/values/strings.xml b/V2rayNG/app/src/main/res/values/strings.xml index b96ce5f2..fe3da122 100644 --- a/V2rayNG/app/src/main/res/values/strings.xml +++ b/V2rayNG/app/src/main/res/values/strings.xml @@ -172,8 +172,11 @@ VPN Settings Per-app proxy General: checked apps use the proxy, and unchecked apps connect directly;\nBypass mode: checked apps connect directly, and unchecked apps use the proxy.\nThe option to automatically select apps that should use the proxy is in the menu. - Auto connect at startup - Automatically connects to the selected server at startup, which may be unsuccessful + Connect after device boot + Automatically connect to the selected server after the device starts. Android may restrict background starts. + System VPN settings + Open Android VPN settings to configure Always-on VPN, if supported by this device. + System VPN settings are unavailable. The task will run in the background, and progress information will be displayed in the notification. diff --git a/V2rayNG/app/src/test/java/com/v2ray/ang/ui/settings/SettingsViewModelTest.kt b/V2rayNG/app/src/test/java/com/v2ray/ang/ui/settings/SettingsViewModelTest.kt new file mode 100644 index 00000000..15c8dce1 --- /dev/null +++ b/V2rayNG/app/src/test/java/com/v2ray/ang/ui/settings/SettingsViewModelTest.kt @@ -0,0 +1,44 @@ +package com.v2ray.ang.ui.settings + +import android.app.Application +import android.content.ComponentName +import android.content.Intent +import android.content.pm.PackageManager +import android.provider.Settings +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.Mockito.mockConstruction +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class SettingsViewModelTest { + @Test + fun vpnSettingsVisibilityFollowsActivityAvailability() = runBlocking(Dispatchers.IO) { + val packageManager = mock() + val application = mock() + whenever(application.packageManager).thenReturn(packageManager) + var activity: ComponentName? = null + + mockConstruction(Intent::class.java) { intent, context -> + assertEquals(listOf(Settings.ACTION_VPN_SETTINGS), context.arguments()) + whenever(intent.resolveActivity(packageManager)).thenAnswer { activity } + }.use { + val viewModel = SettingsViewModel(application) + assertFalse(viewModel.systemVpnSettingsAvailable.value) + viewModel.refreshSystemVpnSettingsAvailability() + assertFalse(viewModel.systemVpnSettingsAvailable.value) + + activity = mock() + viewModel.refreshSystemVpnSettingsAvailability() + assertTrue(viewModel.systemVpnSettingsAvailable.value) + + activity = null + viewModel.refreshSystemVpnSettingsAvailability() + assertFalse(viewModel.systemVpnSettingsAvailable.value) + } + } +}