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>
This commit is contained in:
Copilotand2dust authored and GitHub committed 2026-04-30 10:52:14 +08:00
1 parent e8585a82c6
commit 3b4d895c1d
21 files changed
+238 -103

No files matched your search

+5
View File
@@ -273,6 +273,11 @@
</provider>
<service
android:name="androidx.work.multiprocess.RemoteWorkManagerService"
android:exported="false"
android:process=":bg" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.cache"
@@ -43,10 +43,8 @@ object AppConfig {
const val PREF_FRAGMENT_PACKETS = "pref_fragment_packets"
const val PREF_FRAGMENT_LENGTH = "pref_fragment_length"
const val PREF_FRAGMENT_INTERVAL = "pref_fragment_interval"
const val SUBSCRIPTION_AUTO_UPDATE = "pref_auto_update_subscription"
const val SUBSCRIPTION_AUTO_UPDATE_INTERVAL = "pref_auto_update_interval"
const val SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL = "1440" // Default is 24 hours
const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater"
const val SUBSCRIPTION_MIN_INTERVAL_MINUTES = 15L
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
@@ -7,7 +7,7 @@ data class SubscriptionItem(
val addedTime: Long = System.currentTimeMillis(),
var lastUpdated: Long = -1,
var autoUpdate: Boolean = false,
val updateInterval: Int? = null,
var updateInterval: Long = 1440, // in minutes, default to 24 hours
var prevProfile: String? = null,
var nextProfile: String? = null,
var filter: String? = null,
@@ -679,6 +679,7 @@ object MmkvManager {
return settingsStorage.decodeStringSet(key)
}
/**
* Encodes the start on boot setting.
*
@@ -252,12 +252,7 @@ object SettingsManager {
* it creates a new default subscription to ensure that ungroup
**/
fun removeSubscriptionWithDefault(subid: String) {
// val subsList = decodeSubsList()
// if (subsList.size == 1 && subsList.first() == DEFAULT_SUBSCRIPTION_ID) {
// LogUtil.i(ANG_PACKAGE,"Attempted to remove the only existing default subscription, operation ignored.")
// return
// }
SubscriptionUpdater.cancelOne(subId = subid)
// Remove the subscription
removeSubscription(subid)
@@ -499,7 +494,6 @@ object SettingsManager {
ensureDefaultValue(AppConfig.PREF_MODE, VPN)
ensureDefaultValue(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
ensureDefaultValue(AppConfig.PREF_VPN_MTU, AppConfig.VPN_MTU.toString())
ensureDefaultValue(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
ensureDefaultValue(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
ensureDefaultValue(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
ensureDefaultValue(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
@@ -7,14 +7,149 @@ import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkerParameters
import androidx.work.multiprocess.RemoteWorkManager
import androidx.work.workDataOf
import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.dto.SubscriptionCache
import com.v2ray.ang.util.LogUtil
import java.util.concurrent.TimeUnit
object SubscriptionUpdater {
// -------------------------------------------------------------------------
// Public API — the only methods external callers should ever use
// -------------------------------------------------------------------------
/**
* Sync all subscription tasks with current settings.
*
* Startup/boot callers should use the default mode so existing periodic work is kept.
* Use forceReschedule=true only when the next run time needs to be recalculated from
* the latest persisted subscription state (for example after a manual refresh).
* Call from: MainActivity.onCreate(), BootReceiver.onReceive().
*/
fun sync(
context: Context = AngApplication.application,
forceReschedule: Boolean = false
) {
val existingWorkPolicy =
if (forceReschedule) {
ExistingPeriodicWorkPolicy.REPLACE
} else {
ExistingPeriodicWorkPolicy.KEEP
}
MmkvManager.decodeSubscriptions().forEach { sub ->
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<UpdateTask>(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()
}
@@ -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)
}
}
@@ -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) {
@@ -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<EditTextPreference>(AppConfig.PREF_FRAGMENT_LENGTH) }
private val fragmentInterval by lazy { findPreference<EditTextPreference>(AppConfig.PREF_FRAGMENT_INTERVAL) }
private val autoUpdateCheck by lazy { findPreference<CheckBoxPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE) }
private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
private val hevTunLogLevel by lazy { findPreference<ListPreference>(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
@@ -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
@@ -146,7 +146,6 @@
android:layout_weight="1.0"
android:text="@string/sub_auto_update" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/auto_update_check"
android:layout_width="wrap_content"
@@ -157,6 +156,26 @@
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp16"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_pref_auto_update_interval" />
<EditText
android:id="@+id/et_update_interval"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
tools:ignore="TextFields" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -300,6 +300,7 @@
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
<string name="toast_invalid_update_interval">فاصل التحديث غير صالح. الحد الأدنى هو 15 دقيقة.</string>
<string name="title_sub_update">تحديث الاشتراك (أول خطوة)</string>
<string name="title_ping_all_server">Tcping لجميع الإعدادات</string>
<string name="title_real_ping_all_server"> اختبر جميع الإعدادات (3)</string>
@@ -300,6 +300,7 @@
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
<string name="toast_invalid_update_interval">আপডেটের বিরতি সঠিক নয়। সর্বনিম্ন ১৫ মিনিট।</string>
<string name="title_sub_update">সাবস্ক্রিপশন আপডেট</string>
<string name="title_ping_all_server">সব কনফিগারেশন TCPing</string>
<string name="title_real_ping_all_server">সব কনফিগারেশন প্রকৃত বিলম্ব</string>
@@ -300,6 +300,7 @@
<string name="sub_setting_pre_profile">نوم موستعار پروکسی دیندایی</string>
<string name="sub_setting_next_profile">نوم موستعار پروکسی نیایی</string>
<string name="sub_setting_pre_profile_tip">موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی</string>
<string name="toast_invalid_update_interval">فاسله ورۊ کردن نا موئتبر هڌ. اقل مقدار 15 دؽقه هڌ.</string>
<string name="title_sub_update">ورۊ کردن اشتراک بونکۊ سکویی</string>
<string name="title_ping_all_server">Tcping کانفیگا بونکۊ سکویی</string>
<string name="title_real_ping_all_server">تئخیر واقعی کانفیگا بونکۊ سکویی</string>
@@ -297,6 +297,7 @@
<string name="sub_setting_pre_profile">نام مستعار پروکسی قبلی</string>
<string name="sub_setting_next_profile">نام مستعار پروکسی بعدی</string>
<string name="sub_setting_pre_profile_tip">لطفاً مطمئن شوید که نام مستعار وجود دارد و منحصر به فرد است</string>
<string name="toast_invalid_update_interval">بازه بروزرسانی نامعتبر است. حداقل مقدار 15 دقیقه است.</string>
<string name="title_sub_update">به‌روزرسانی گروه فعلی اشتراک</string>
<string name="title_ping_all_server">TCPING کانفیگ های گروه فعلی</string>
<string name="title_real_ping_all_server">تاخیر واقعی کانفیگ های گروه فعلی</string>
@@ -303,6 +303,7 @@
<string name="sub_setting_pre_profile">Предыдущий профиль прокси</string>
<string name="sub_setting_next_profile">Следующий профиль прокси</string>
<string name="sub_setting_pre_profile_tip">Профиль должен быть уникальным</string>
<string name="toast_invalid_update_interval">Неверный интервал обновления. Минимум 15 минут.</string>
<string name="title_sub_update">Обновить подписку</string>
<string name="title_ping_all_server">Проверить профили</string>
<string name="title_real_ping_all_server">Проверить задержку профилей</string>
@@ -300,6 +300,7 @@
<string name="sub_setting_pre_profile">Previous proxy configuration remarks</string>
<string name="sub_setting_next_profile">Next proxy configuration remarks</string>
<string name="sub_setting_pre_profile_tip">The configuration remarks exists and is unique</string>
<string name="toast_invalid_update_interval">Khoảng thời gian cập nhật không hợp lệ. Giá trị tối thiểu là 15 phút.</string>
<string name="title_sub_update">Cập nhật các gói đăng ký</string>
<string name="title_ping_all_server">Ping tất cả máy chủ</string>
<string name="title_real_ping_all_server">Kiểm tra HTTP tất cả máy chủ</string>
@@ -298,6 +298,7 @@
<string name="sub_setting_pre_profile">前置代理配置别名</string>
<string name="sub_setting_next_profile">落地代理配置別名</string>
<string name="sub_setting_pre_profile_tip">请确保配置别名存在并唯一</string>
<string name="toast_invalid_update_interval">更新间隔无效,最小值为 15 分钟。</string>
<string name="title_sub_update">更新订阅</string>
<string name="title_ping_all_server">测试配置 Tcping</string>
<string name="title_real_ping_all_server">测试配置真连接</string>
@@ -299,6 +299,7 @@
<string name="sub_setting_pre_profile">前置代理設定檔别名</string>
<string name="sub_setting_next_profile">落地代理設定檔別名</string>
<string name="sub_setting_pre_profile_tip">请确保設定檔别名存在并唯一</string>
<string name="toast_invalid_update_interval">更新間隔無效,最小值為 15 分鐘。</string>
<string name="title_sub_update">更新訂閱</string>
<string name="title_ping_all_server">偵測設定 Tcping</string>
<string name="title_real_ping_all_server">偵測設定真延遲</string>
@@ -305,6 +305,7 @@
<string name="sub_setting_pre_profile">Previous proxy config remarks</string>
<string name="sub_setting_next_profile">Next proxy config remarks</string>
<string name="sub_setting_pre_profile_tip">The config remarks exist and are unique</string>
<string name="toast_invalid_update_interval">Invalid update interval. Minimum is 15 minutes.</string>
<string name="title_sub_update">Update subscription</string>
<string name="title_ping_all_server">Tcping config</string>
<string name="title_real_ping_all_server">Real delay config</string>
@@ -278,19 +278,6 @@
android:title="@string/title_pref_fragment_packets" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/title_sub_setting"
app:initialExpandedChildrenCount="0">
<CheckBoxPreference
android:key="pref_auto_update_subscription"
android:summary="@string/summary_pref_auto_update_subscription"
android:title="@string/title_pref_auto_update_subscription" />
<EditTextPreference
android:inputType="number"
android:key="pref_auto_update_interval"
android:summary="1440"
android:title="@string/title_pref_auto_update_interval" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/title_advanced">