From 3b4d895c1dcefaeaf46b40cb62c6e21840165c82 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:52:14 +0800 Subject: [PATCH] Refactor: centralize subscription auto-update scheduling in SubscriptionUpdater (#5567) * Initial plan * Refactor subscription auto-update system into SubscriptionUpdater facade Agent-Logs-Url: https://github.com/2dust/v2rayNG/sessions/63125c89-4db9-4572-8adb-10c29ea531f5 Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com> * Add TODO comment to legacy fallback in SubscriptionUpdater Agent-Logs-Url: https://github.com/2dust/v2rayNG/sessions/63125c89-4db9-4572-8adb-10c29ea531f5 Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com> * Use per-subscription auto-update intervals Migrate subscription auto-update from a global toggle/interval to per-subscription scheduling. - Add per-subscription updateInterval (Long, default 1440 minutes) and persist in SubEdit UI; validate minimum interval (15 minutes). - Update SubscriptionUpdater to schedule/cancel tasks per subscription, compute initial delay from subscription.lastUpdated, and skip when autoUpdate is disabled or subId is missing. - Remove legacy global auto-update prefs and fallback logic: deleted global prefs/UI handling, MmkvManager last-attempt encode/decode, and related default setting. - Ensure cancelling of scheduled task when removing a subscription and adjust SettingsManager initialization. - Add localized toast strings for invalid update interval and update related layouts/resources. This change enables individual update intervals per subscription and simplifies the updater by removing legacy global state and last-attempt persistence. * Allow optional force-reschedule for subscriptions Add a forceReschedule flag to SubscriptionUpdater.sync to choose the WorkManager policy (KEEP by default, REPLACE when forced). Refactor scheduleOne to accept an ExistingPeriodicWorkPolicy and use it when enqueuing periodic work; syncOne now forces REPLACE so a manual refresh recalculates next run. Remove the cancelAll helper and improve logging to include the reschedule flag/policy. This lets startup keep existing periodic work while allowing explicit reschedules when needed (e.g., after manual updates). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com> --- V2rayNG/app/src/main/AndroidManifest.xml | 5 + .../src/main/java/com/v2ray/ang/AppConfig.kt | 4 +- .../com/v2ray/ang/dto/SubscriptionItem.kt | 2 +- .../java/com/v2ray/ang/handler/MmkvManager.kt | 1 + .../com/v2ray/ang/handler/SettingsManager.kt | 8 +- .../v2ray/ang/handler/SubscriptionUpdater.kt | 196 +++++++++++++++--- .../com/v2ray/ang/receiver/BootReceiver.kt | 2 + .../java/com/v2ray/ang/ui/MainActivity.kt | 2 + .../java/com/v2ray/ang/ui/SettingsActivity.kt | 53 ----- .../java/com/v2ray/ang/ui/SubEditActivity.kt | 25 +++ .../src/main/res/layout/activity_sub_edit.xml | 21 +- .../app/src/main/res/values-ar/strings.xml | 1 + .../app/src/main/res/values-bn/strings.xml | 1 + .../src/main/res/values-bqi-rIR/strings.xml | 1 + .../app/src/main/res/values-fa/strings.xml | 1 + .../app/src/main/res/values-ru/strings.xml | 1 + .../app/src/main/res/values-vi/strings.xml | 1 + .../src/main/res/values-zh-rCN/strings.xml | 1 + .../src/main/res/values-zh-rTW/strings.xml | 1 + V2rayNG/app/src/main/res/values/strings.xml | 1 + .../app/src/main/res/xml/pref_settings.xml | 13 -- 21 files changed, 238 insertions(+), 103 deletions(-) diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml index 9d2dd6e8..59a167b3 100644 --- a/V2rayNG/app/src/main/AndroidManifest.xml +++ b/V2rayNG/app/src/main/AndroidManifest.xml @@ -273,6 +273,11 @@ + + + scheduleOne( + context = context, + subId = sub.guid, + shouldRun = sub.subscription.autoUpdate, + existingWorkPolicy = existingWorkPolicy + ) + } + LogUtil.i( + AppConfig.TAG, + "SubscriptionUpdater: sync complete forceReschedule=$forceReschedule" + ) + } + + /** + * Sync a single subscription's task. + * Call from: SubEditActivity after saving, after a manual update (to reset the timer). + */ + fun syncOne(context: Context = AngApplication.application, subId: String) { + val subItem = MmkvManager.decodeSubscription(subId) ?: return + scheduleOne( + context = context, + subId = subId, + shouldRun = subItem.autoUpdate, + existingWorkPolicy = ExistingPeriodicWorkPolicy.REPLACE + ) + } + + /** + * Cancel the auto-update task for a single subscription. + * Call from: when a subscription is deleted. + */ + fun cancelOne(context: Context = AngApplication.application, subId: String) { + RemoteWorkManager.getInstance(context) + .cancelUniqueWork(taskName(subId)) + } + + // ------------------------------------------------------------------------- + // Internal scheduling logic + // ------------------------------------------------------------------------- + + private fun taskName(subId: String) = "${AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME}_$subId" + + private fun scheduleOne( + context: Context, + subId: String, + shouldRun: Boolean, + existingWorkPolicy: ExistingPeriodicWorkPolicy + ) { + val rw = RemoteWorkManager.getInstance(context) + if (!shouldRun) { + rw.cancelUniqueWork(taskName(subId)) + LogUtil.d(AppConfig.TAG, "SubscriptionUpdater: cancelled task for $subId") + return + } + + val subItem = MmkvManager.decodeSubscription(subId) ?: return + + val intervalMinutes = maxOf( + AppConfig.SUBSCRIPTION_MIN_INTERVAL_MINUTES, + subItem.updateInterval + ) + + // Base initial delay on the last successful update time persisted in subscription. + val lastUpdated = subItem.lastUpdated + val intervalMillis = intervalMinutes * 60 * 1000L + val now = System.currentTimeMillis() + val initialDelayMillis = if (lastUpdated <= 0L) { + 0L + } else { + maxOf(0L, lastUpdated + intervalMillis - now) + } + + val request = PeriodicWorkRequestBuilder(intervalMinutes, TimeUnit.MINUTES) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + ) + .setInputData(workDataOf(KEY_SUB_ID to subId)) + .setInitialDelay(initialDelayMillis, TimeUnit.MILLISECONDS) + .addTag(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME) + .build() + + rw.enqueueUniquePeriodicWork( + taskName(subId), + existingWorkPolicy, + request + ) + + LogUtil.i( + AppConfig.TAG, + "SubscriptionUpdater: scheduled [$subId] interval=${intervalMinutes}min " + + "initialDelay=${initialDelayMillis / 1000}s policy=$existingWorkPolicy" + ) + } + + // ------------------------------------------------------------------------- + // Worker + // ------------------------------------------------------------------------- + + private const val KEY_SUB_ID = "subId" + class UpdateTask(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { @@ -23,39 +158,50 @@ object SubscriptionUpdater { NotificationCompat.Builder(applicationContext, AppConfig.SUBSCRIPTION_UPDATE_CHANNEL) .setWhen(0) .setTicker("Update") - .setContentTitle(context.getString(R.string.title_pref_auto_update_subscription)) + .setContentTitle(applicationContext.getString(R.string.title_pref_auto_update_subscription)) .setSmallIcon(R.drawable.ic_stat_name) .setCategory(NotificationCompat.CATEGORY_SERVICE) .setPriority(NotificationCompat.PRIORITY_DEFAULT) - /** - * Performs the subscription update work. - * @return The result of the work. - */ @SuppressLint("MissingPermission") override suspend fun doWork(): Result { - LogUtil.i(AppConfig.TAG, "subscription automatic update starting") + val subId = inputData.getString(KEY_SUB_ID) + LogUtil.i(AppConfig.TAG, "SubscriptionUpdater automatic update starting: $subId") - val subs = MmkvManager.decodeSubscriptions().filter { it.subscription.autoUpdate } - - for (sub in subs) { - val subItem = sub.subscription - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - notification.setChannelId(AppConfig.SUBSCRIPTION_UPDATE_CHANNEL) - val channel = - NotificationChannel( - AppConfig.SUBSCRIPTION_UPDATE_CHANNEL, - AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME, - NotificationManager.IMPORTANCE_MIN - ) - notificationManager.createNotificationChannel(channel) - } - notificationManager.notify(3, notification.build()) - LogUtil.i(AppConfig.TAG, "subscription automatic update: ---${subItem.remarks}") - AngConfigManager.updateConfigViaSub(sub) - notification.setContentText("Updating ${subItem.remarks}") + if (subId.isNullOrEmpty()) { + LogUtil.w(AppConfig.TAG, "SubscriptionUpdater: missing subId in worker input") + return Result.success() } + + + val subItem = MmkvManager.decodeSubscription(subId) + if (subItem == null) { + LogUtil.w(AppConfig.TAG, "SubscriptionUpdater: no subscription found for $subId") + return Result.success() + } + + if (!subItem.autoUpdate) { + LogUtil.i(AppConfig.TAG, "SubscriptionUpdater: auto-update disabled for $subId, skip") + return Result.success() + } + + val sub = SubscriptionCache(subId, subItem) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + notificationManager.createNotificationChannel( + NotificationChannel( + AppConfig.SUBSCRIPTION_UPDATE_CHANNEL, + AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME, + NotificationManager.IMPORTANCE_MIN + ) + ) + } + + notificationManager.notify(3, notification.build()) + LogUtil.i(AppConfig.TAG, "SubscriptionUpdater automatic update: ---${sub.subscription.remarks}") + AngConfigManager.updateConfigViaSub(sub) + notification.setContentText("Updating ${sub.subscription.remarks}") + notificationManager.cancel(3) return Result.success() } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt index 3d00f0eb..c34c067f 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import com.v2ray.ang.AppConfig import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SubscriptionUpdater import com.v2ray.ang.handler.V2RayServiceManager import com.v2ray.ang.util.LogUtil @@ -37,5 +38,6 @@ class BootReceiver : BroadcastReceiver() { LogUtil.i(AppConfig.TAG, "BootReceiver: Starting V2Ray service") V2RayServiceManager.startVService(context) + SubscriptionUpdater.sync(context) } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt index 7df9bfd8..4c8a8466 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt @@ -31,6 +31,7 @@ import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsChangeManager import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.handler.SubscriptionUpdater import com.v2ray.ang.handler.V2RayServiceManager import com.v2ray.ang.util.LogUtil import com.v2ray.ang.util.Utils @@ -98,6 +99,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect setupGroupTab() setupViewModel() + SubscriptionUpdater.sync() mainViewModel.reloadServerList() checkAndRequestPermission(PermissionType.POST_NOTIFICATIONS) { diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt index fba3ce1d..9c281231 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt @@ -6,19 +6,12 @@ import androidx.preference.CheckBoxPreference import androidx.preference.EditTextPreference import androidx.preference.ListPreference import androidx.preference.PreferenceFragmentCompat -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.PeriodicWorkRequest -import androidx.work.multiprocess.RemoteWorkManager -import com.v2ray.ang.AngApplication import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig.VPN import com.v2ray.ang.R -import com.v2ray.ang.extension.toLongEx import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.handler.SubscriptionUpdater import com.v2ray.ang.helper.MmkvPreferenceDataStore import com.v2ray.ang.util.Utils -import java.util.concurrent.TimeUnit class SettingsActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -48,8 +41,6 @@ class SettingsActivity : BaseActivity() { private val fragmentLength by lazy { findPreference(AppConfig.PREF_FRAGMENT_LENGTH) } private val fragmentInterval by lazy { findPreference(AppConfig.PREF_FRAGMENT_INTERVAL) } - private val autoUpdateCheck by lazy { findPreference(AppConfig.SUBSCRIPTION_AUTO_UPDATE) } - private val autoUpdateInterval by lazy { findPreference(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) } private val mode by lazy { findPreference(AppConfig.PREF_MODE) } private val hevTunLogLevel by lazy { findPreference(AppConfig.PREF_HEV_TUNNEL_LOGLEVEL) } @@ -96,24 +87,6 @@ class SettingsActivity : BaseActivity() { true } - autoUpdateCheck?.setOnPreferenceChangeListener { _, newValue -> - val value = newValue as Boolean - autoUpdateCheck?.isChecked = value - autoUpdateInterval?.isEnabled = value - autoUpdateInterval?.text?.toLongEx()?.let { - if (newValue) configureUpdateTask(it) else cancelUpdateTask() - } - true - } - - autoUpdateInterval?.setOnPreferenceChangeListener { _, newValue -> - val interval = (newValue as? String)?.toLongEx() - if (interval != null && autoUpdateCheck?.isChecked == true) { - configureUpdateTask(interval) - } - true - } - mode?.setOnPreferenceChangeListener { pref, newValue -> val valueStr = newValue.toString() (pref as? ListPreference)?.let { lp -> @@ -203,9 +176,6 @@ class SettingsActivity : BaseActivity() { // Initialize fragment-dependent UI states updateFragment(MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false)) - // Initialize auto-update interval state - autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) - updateDynamicSocksPort(MmkvManager.decodeSettingsBool(AppConfig.PREF_DYNAMIC_SOCKS_PORT, false)) } @@ -243,29 +213,6 @@ class SettingsActivity : BaseActivity() { vpnDns?.isEnabled = !enabled } - private fun configureUpdateTask(interval: Long) { - val rw = RemoteWorkManager.getInstance(AngApplication.application) - rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME) - rw.enqueueUniquePeriodicWork( - AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME, - ExistingPeriodicWorkPolicy.UPDATE, - PeriodicWorkRequest.Builder( - SubscriptionUpdater.UpdateTask::class.java, - interval, - TimeUnit.MINUTES - ) - .apply { - setInitialDelay(interval, TimeUnit.MINUTES) - } - .build() - ) - } - - private fun cancelUpdateTask() { - val rw = RemoteWorkManager.getInstance(AngApplication.application) - rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME) - } - private fun updateMux(enabled: Boolean) { muxConcurrency?.isEnabled = enabled muxXudpConcurrency?.isEnabled = enabled diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt index c6bd3870..a2b113fe 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt @@ -15,6 +15,7 @@ import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsChangeManager import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.handler.SubscriptionUpdater import com.v2ray.ang.util.Utils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -51,6 +52,7 @@ class SubEditActivity : BaseActivity() { binding.etFilter.text = Utils.getEditable(subItem.filter) binding.chkEnable.isChecked = subItem.enabled binding.autoUpdateCheck.isChecked = subItem.autoUpdate + binding.etUpdateInterval.text = Utils.getEditable(subItem.updateInterval.toString()) binding.allowInsecureUrl.isChecked = subItem.allowInsecureUrl binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile) binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile) @@ -65,6 +67,7 @@ class SubEditActivity : BaseActivity() { binding.etUrl.text = null binding.etFilter.text = null binding.chkEnable.isChecked = true + binding.etUpdateInterval.text = null binding.etPreProfile.text = null binding.etNextProfile.text = null return true @@ -82,6 +85,27 @@ class SubEditActivity : BaseActivity() { subItem.filter = binding.etFilter.text.toString() subItem.enabled = binding.chkEnable.isChecked subItem.autoUpdate = binding.autoUpdateCheck.isChecked + + val intervalInput = binding.etUpdateInterval.text.toString().trim() + val intervalMinutes = intervalInput.toLongOrNull() + if (subItem.autoUpdate) { + // autoUpdate is enabled: interval must be valid + if (intervalMinutes == null) { + // field is empty, reset to default + subItem.updateInterval = SubscriptionItem().updateInterval + } else if (intervalMinutes < AppConfig.SUBSCRIPTION_MIN_INTERVAL_MINUTES) { + toast(R.string.toast_invalid_update_interval) + return false + } else { + subItem.updateInterval = intervalMinutes + } + } else { + // autoUpdate is disabled: save only if the value is valid, otherwise keep the existing value + if (intervalMinutes != null && intervalMinutes >= AppConfig.SUBSCRIPTION_MIN_INTERVAL_MINUTES) { + subItem.updateInterval = intervalMinutes + } + } + subItem.prevProfile = binding.etPreProfile.text.toString() subItem.nextProfile = binding.etNextProfile.text.toString() subItem.allowInsecureUrl = binding.allowInsecureUrl.isChecked @@ -105,6 +129,7 @@ class SubEditActivity : BaseActivity() { } MmkvManager.encodeSubscription(editSubId, subItem) + SubscriptionUpdater.syncOne(subId = editSubId) toastSuccess(R.string.toast_success) finish() return true diff --git a/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml b/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml index 87a44c58..0ac5f9fa 100644 --- a/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml +++ b/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml @@ -146,7 +146,6 @@ android:layout_weight="1.0" android:text="@string/sub_auto_update" /> - + + + + + + + + Previous proxy configuration remarks Next proxy configuration remarks The configuration remarks exists and is unique + فاصل التحديث غير صالح. الحد الأدنى هو 15 دقيقة. تحديث الاشتراك (أول خطوة) Tcping لجميع الإعدادات اختبر جميع الإعدادات (3) diff --git a/V2rayNG/app/src/main/res/values-bn/strings.xml b/V2rayNG/app/src/main/res/values-bn/strings.xml index 63fec7ec..4b7103c9 100644 --- a/V2rayNG/app/src/main/res/values-bn/strings.xml +++ b/V2rayNG/app/src/main/res/values-bn/strings.xml @@ -300,6 +300,7 @@ Previous proxy configuration remarks Next proxy configuration remarks The configuration remarks exists and is unique + আপডেটের বিরতি সঠিক নয়। সর্বনিম্ন ১৫ মিনিট। সাবস্ক্রিপশন আপডেট সব কনফিগারেশন TCPing সব কনফিগারেশন প্রকৃত বিলম্ব 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 9d1c2904..ba3cad76 100644 --- a/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml +++ b/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml @@ -300,6 +300,7 @@ نوم موستعار پروکسی دیندایی نوم موستعار پروکسی نیایی موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی + فاسله ورۊ کردن نا موئتبر هڌ. اقل مقدار 15 دؽقه هڌ. ورۊ کردن اشتراک بونکۊ سکویی Tcping کانفیگا بونکۊ سکویی تئخیر واقعی کانفیگا بونکۊ سکویی diff --git a/V2rayNG/app/src/main/res/values-fa/strings.xml b/V2rayNG/app/src/main/res/values-fa/strings.xml index 581e90e5..2de37668 100644 --- a/V2rayNG/app/src/main/res/values-fa/strings.xml +++ b/V2rayNG/app/src/main/res/values-fa/strings.xml @@ -297,6 +297,7 @@ نام مستعار پروکسی قبلی نام مستعار پروکسی بعدی لطفاً مطمئن شوید که نام مستعار وجود دارد و منحصر به فرد است + بازه بروزرسانی نامعتبر است. حداقل مقدار 15 دقیقه است. به‌روزرسانی گروه فعلی اشتراک TCPING کانفیگ های گروه فعلی تاخیر واقعی کانفیگ های گروه فعلی diff --git a/V2rayNG/app/src/main/res/values-ru/strings.xml b/V2rayNG/app/src/main/res/values-ru/strings.xml index 623017ab..9c0f106e 100644 --- a/V2rayNG/app/src/main/res/values-ru/strings.xml +++ b/V2rayNG/app/src/main/res/values-ru/strings.xml @@ -303,6 +303,7 @@ Предыдущий профиль прокси Следующий профиль прокси Профиль должен быть уникальным + Неверный интервал обновления. Минимум 15 минут. Обновить подписку Проверить профили Проверить задержку профилей diff --git a/V2rayNG/app/src/main/res/values-vi/strings.xml b/V2rayNG/app/src/main/res/values-vi/strings.xml index 8232249d..6098c3d3 100644 --- a/V2rayNG/app/src/main/res/values-vi/strings.xml +++ b/V2rayNG/app/src/main/res/values-vi/strings.xml @@ -300,6 +300,7 @@ Previous proxy configuration remarks Next proxy configuration remarks The configuration remarks exists and is unique + Khoảng thời gian cập nhật không hợp lệ. Giá trị tối thiểu là 15 phút. Cập nhật các gói đăng ký Ping tất cả máy chủ Kiểm tra HTTP tất cả máy chủ 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 53ad90d1..3587e7e7 100644 --- a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml +++ b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml @@ -298,6 +298,7 @@ 前置代理配置别名 落地代理配置別名 请确保配置别名存在并唯一 + 更新间隔无效,最小值为 15 分钟。 更新订阅 测试配置 Tcping 测试配置真连接 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 8efd19fa..0205150c 100644 --- a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml +++ b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml @@ -299,6 +299,7 @@ 前置代理設定檔别名 落地代理設定檔別名 请确保設定檔别名存在并唯一 + 更新間隔無效,最小值為 15 分鐘。 更新訂閱 偵測設定 Tcping 偵測設定真延遲 diff --git a/V2rayNG/app/src/main/res/values/strings.xml b/V2rayNG/app/src/main/res/values/strings.xml index 10a170ac..e70a7cae 100644 --- a/V2rayNG/app/src/main/res/values/strings.xml +++ b/V2rayNG/app/src/main/res/values/strings.xml @@ -305,6 +305,7 @@ Previous proxy config remarks Next proxy config remarks The config remarks exist and are unique + Invalid update interval. Minimum is 15 minutes. Update subscription Tcping config Real delay config diff --git a/V2rayNG/app/src/main/res/xml/pref_settings.xml b/V2rayNG/app/src/main/res/xml/pref_settings.xml index 19a086b1..0095bc76 100644 --- a/V2rayNG/app/src/main/res/xml/pref_settings.xml +++ b/V2rayNG/app/src/main/res/xml/pref_settings.xml @@ -278,19 +278,6 @@ android:title="@string/title_pref_fragment_packets" /> - - - -