diff --git a/V2rayNG/app/build.gradle b/V2rayNG/app/build.gradle
index 6243219a..00cc729b 100644
--- a/V2rayNG/app/build.gradle
+++ b/V2rayNG/app/build.gradle
@@ -132,4 +132,9 @@ dependencies {
implementation 'com.blacksquircle.ui:language-json:2.8.0'
implementation 'io.github.g00fy2.quickie:quickie-bundled:1.6.0'
implementation 'com.google.zxing:core:3.5.1'
+
+ def work_version = "2.8.1"
+
+ implementation "androidx.work:work-runtime-ktx:$work_version"
+ implementation "androidx.work:work-multiprocess:$work_version"
}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml
index b2d5aede..bdec513e 100644
--- a/V2rayNG/app/src/main/AndroidManifest.xml
+++ b/V2rayNG/app/src/main/AndroidManifest.xml
@@ -192,6 +192,18 @@
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt
index aa1ef7d4..f9de6ca6 100644
--- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt
@@ -1,12 +1,20 @@
package com.v2ray.ang
+import android.content.Context
import androidx.multidex.MultiDexApplication
import androidx.preference.PreferenceManager
+import androidx.work.Configuration
import com.tencent.mmkv.MMKV
-class AngApplication : MultiDexApplication() {
+class AngApplication : MultiDexApplication(), Configuration.Provider {
companion object {
const val PREF_LAST_VERSION = "pref_last_version"
+ lateinit var application: AngApplication
+ }
+
+ override fun attachBaseContext(base: Context?) {
+ super.attachBaseContext(base)
+ application = this
}
var firstRun = false
@@ -25,4 +33,10 @@ class AngApplication : MultiDexApplication() {
//Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE)
MMKV.initialize(this)
}
+
+ override fun getWorkManagerConfiguration(): Configuration {
+ return Configuration.Builder()
+ .setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
+ .build()
+ }
}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt
index ee5d58ff..8a3be9e0 100644
--- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt
@@ -90,4 +90,11 @@ object AppConfig {
const val MSG_MEASURE_CONFIG = 7
const val MSG_MEASURE_CONFIG_SUCCESS = 71
const val MSG_MEASURE_CONFIG_CANCEL = 72
+
+ // subscription settings
+ const val SUBSCRIPTION_AUTO_UPDATE = "pref_auto_update_subscription"
+ const val SUBSCRIPTION_AUTO_UPDATE_INTERVAL = "pref_auto_update_interval"
+ const val DEFAULT_UPDATE_INTERVAL = "1440" // 24 hours
+ const val UPDATE_TASK_NAME = "subscription-updater"
+
}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt
index b2195148..5ead1d5a 100644
--- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt
@@ -1,8 +1,11 @@
package com.v2ray.ang.dto
data class SubscriptionItem(
- var remarks: String = "",
- var url: String = "",
- var enabled: Boolean = true,
- val addedTime: Long = System.currentTimeMillis()) {
-}
+ var remarks: String = "",
+ var url: String = "",
+ var enabled: Boolean = true,
+ val addedTime: Long = System.currentTimeMillis(),
+ var lastUpdated: Long = -1,
+ var autoUpdate: Boolean = false,
+ val updateInterval: Int? = null,
+)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/SubscriptionUpdater.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/SubscriptionUpdater.kt
new file mode 100644
index 00000000..4eeaaa7e
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/SubscriptionUpdater.kt
@@ -0,0 +1,83 @@
+package com.v2ray.ang.service
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import android.util.Log
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.util.AngConfigManager
+import com.v2ray.ang.util.MmkvManager
+import com.v2ray.ang.util.Utils
+import kotlinx.coroutines.delay
+
+object SubscriptionUpdater {
+
+ const val notificationChannel = "update-subscription-channel"
+
+ class UpdateTask(context: Context, params: WorkerParameters) :
+ CoroutineWorker(context, params) {
+
+ private val notificationManager = NotificationManagerCompat.from(applicationContext)
+ private val notification =
+ NotificationCompat.Builder(applicationContext, notificationChannel)
+
+ .setWhen(0)
+ .setTicker("Update")
+ .setContentTitle("Subscription Update")
+ .setSmallIcon(R.drawable.ic_stat_name)
+ .setCategory(NotificationCompat.CATEGORY_SERVICE)
+ .setPriority(NotificationCompat.PRIORITY_MIN)
+
+ @SuppressLint("MissingPermission")
+ override suspend fun doWork(): Result {
+ Log.d(AppConfig.ANG_PACKAGE, "start updating subscriptions")
+
+
+ val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate }
+
+ for (i in subs) {
+
+ val subscription = i.second
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ notification.setChannelId(notificationChannel)
+ val channel =
+ NotificationChannel(
+ notificationChannel,
+ "Subscription Update Service",
+ NotificationManager.IMPORTANCE_MIN
+ )
+ notificationManager.createNotificationChannel(channel)
+ }
+ notificationManager.notify(3, notification.build())
+ Log.d(AppConfig.ANG_PACKAGE, "update: ${subscription.remarks} subscription")
+ val configs = Utils.getUrlContentWithCustomUserAgent(subscription.url)
+ importBatchConfig(configs, i.first)
+ notification.setContentText("Updating ${subscription.remarks}")
+ }
+ notificationManager.cancel(3)
+ return Result.success()
+ }
+ }
+
+ fun importBatchConfig(server: String?, subid: String = "") {
+
+ val append = subid.isNullOrEmpty()
+
+ var count = AngConfigManager.importBatchConfig(server, subid, append)
+ if (count <= 0) {
+ AngConfigManager.importBatchConfig(Utils.decode(server!!), subid, append)
+ }
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt
index b07d5c7a..e2420ce3 100644
--- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt
@@ -6,10 +6,21 @@ import android.text.TextUtils
import android.view.View
import androidx.activity.viewModels
import androidx.preference.*
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequest
+import androidx.work.multiprocess.RemoteWorkManager
+import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
+import com.v2ray.ang.dto.AngConfig
+import com.v2ray.ang.service.SubscriptionUpdater
import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.SettingsViewModel
+import java.sql.Time
+import java.util.concurrent.TimeUnit
+import kotlin.time.toDuration
class SettingsActivity : BaseActivity() {
private val settingsViewModel: SettingsViewModel by viewModels()
@@ -29,16 +40,20 @@ class SettingsActivity : BaseActivity() {
private val fakeDns by lazy { findPreference(AppConfig.PREF_FAKE_DNS_ENABLED) }
private val localDnsPort by lazy { findPreference(AppConfig.PREF_LOCAL_DNS_PORT) }
private val vpnDns by lazy { findPreference(AppConfig.PREF_VPN_DNS) }
+
private val mux by lazy { findPreference(AppConfig.PREF_MUX_ENABLED) }
private val muxXudpConcurrency by lazy { findPreference(AppConfig.PREF_MUX_XUDP_CONCURRENCY) }
private val muxXudpQuic by lazy { findPreference(AppConfig.PREF_MUX_XUDP_QUIC) }
+
// val autoRestart by lazy { findPreference(PREF_AUTO_RESTART) as CheckBoxPreference }
private val remoteDns by lazy { findPreference(AppConfig.PREF_REMOTE_DNS) }
private val domesticDns by lazy { findPreference(AppConfig.PREF_DOMESTIC_DNS) }
private val socksPort by lazy { findPreference(AppConfig.PREF_SOCKS_PORT) }
private val httpPort by lazy { findPreference(AppConfig.PREF_HTTP_PORT) }
private val routingCustom by lazy { findPreference(AppConfig.PREF_ROUTING_CUSTOM) }
+ private val autoUpdateCheck by lazy { findPreference(AppConfig.SUBSCRIPTION_AUTO_UPDATE) }
+ private val autoUpdateInterval by lazy { findPreference(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
// val licenses: Preference by lazy { findPreference(PREF_LICENSES) }
// val feedback: Preference by lazy { findPreference(PREF_FEEDBACK) }
// val tgGroup: Preference by lazy { findPreference(PREF_TG_GROUP) }
@@ -53,6 +68,24 @@ class SettingsActivity : BaseActivity() {
false
}
+ autoUpdateCheck?.setOnPreferenceChangeListener { _, newValue ->
+ val value = newValue as Boolean
+ autoUpdateCheck?.isChecked = value
+ autoUpdateInterval?.isEnabled = value
+ autoUpdateInterval?.text?.toLong()?.let {
+ if (newValue) configureUpdateTask(it) else cancelUpdateTask()
+ }
+ true
+ }
+
+ autoUpdateInterval?.setOnPreferenceChangeListener { _, any ->
+ val nval = any as String
+ autoUpdateInterval?.summary =
+ if (TextUtils.isEmpty(nval) or (nval.toLong() < 15)) AppConfig.DEFAULT_UPDATE_INTERVAL else nval
+ configureUpdateTask(nval.toLong())
+ false
+ }
+
// licenses.onClick {
// val fragment = LicensesDialogFragment.Builder(act)
// .setNotices(R.raw.licenses)
@@ -94,13 +127,14 @@ class SettingsActivity : BaseActivity() {
true
}
- localDns?.setOnPreferenceChangeListener{ _, any ->
+ localDns?.setOnPreferenceChangeListener { _, any ->
updateLocalDns(any as Boolean)
true
}
localDnsPort?.setOnPreferenceChangeListener { _, any ->
val nval = any as String
- localDnsPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval
+ localDnsPort?.summary =
+ if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval
true
}
vpnDns?.setOnPreferenceChangeListener { _, any ->
@@ -135,15 +169,18 @@ class SettingsActivity : BaseActivity() {
override fun onStart() {
super.onStart()
- val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
+ val defaultSharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(requireActivity())
updateMode(defaultSharedPreferences.getString(AppConfig.PREF_MODE, "VPN"))
var remoteDnsString = defaultSharedPreferences.getString(AppConfig.PREF_REMOTE_DNS, "")
+
domesticDns?.summary = defaultSharedPreferences.getString(AppConfig.PREF_DOMESTIC_DNS, "")
localDnsPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
socksPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
httpPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
updateMux(defaultSharedPreferences.getBoolean(AppConfig.PREF_MUX_ENABLED, false))
muxXudpConcurrency?.summary = defaultSharedPreferences.getString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
+ autoUpdateInterval?.summary = defaultSharedPreferences.getString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL,AppConfig.DEFAULT_UPDATE_INTERVAL)
if (TextUtils.isEmpty(remoteDnsString)) {
remoteDnsString = AppConfig.DNS_AGENT
@@ -152,7 +189,8 @@ class SettingsActivity : BaseActivity() {
domesticDns?.summary = AppConfig.DNS_DIRECT
}
remoteDns?.summary = remoteDnsString
- vpnDns?.summary = defaultSharedPreferences.getString(AppConfig.PREF_VPN_DNS, remoteDnsString)
+ vpnDns?.summary =
+ defaultSharedPreferences.getString(AppConfig.PREF_VPN_DNS, remoteDnsString)
if (TextUtils.isEmpty(localDnsPort?.summary)) {
localDnsPort?.summary = AppConfig.PORT_LOCAL_DNS
@@ -166,17 +204,24 @@ class SettingsActivity : BaseActivity() {
}
private fun updateMode(mode: String?) {
- val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
+ val defaultSharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(requireActivity())
val vpn = mode == "VPN"
perAppProxy?.isEnabled = vpn
- perAppProxy?.isChecked = PreferenceManager.getDefaultSharedPreferences(requireActivity())
+ perAppProxy?.isChecked =
+ PreferenceManager.getDefaultSharedPreferences(requireActivity())
.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
localDns?.isEnabled = vpn
fakeDns?.isEnabled = vpn
localDnsPort?.isEnabled = vpn
vpnDns?.isEnabled = vpn
if (vpn) {
- updateLocalDns(defaultSharedPreferences.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false))
+ updateLocalDns(
+ defaultSharedPreferences.getBoolean(
+ AppConfig.PREF_LOCAL_DNS_ENABLED,
+ false
+ )
+ )
}
}
@@ -186,6 +231,32 @@ class SettingsActivity : BaseActivity() {
vpnDns?.isEnabled = !enabled
}
+ private fun configureUpdateTask(interval: Long) {
+ val rw = RemoteWorkManager.getInstance(AngApplication.application)
+ rw.cancelUniqueWork(AppConfig.UPDATE_TASK_NAME)
+ rw.enqueueUniquePeriodicWork(
+ AppConfig.UPDATE_TASK_NAME,
+ ExistingPeriodicWorkPolicy.UPDATE,
+ PeriodicWorkRequest.Builder(
+ SubscriptionUpdater.UpdateTask::class.java,
+ interval,
+ TimeUnit.MINUTES
+ )
+ .apply {
+ setInitialDelay(interval, TimeUnit.MINUTES)
+ }
+ .setConstraints(
+ Constraints(
+ NetworkType.CONNECTED,
+ )
+ ).build()
+ )
+ }
+
+ private fun cancelUpdateTask() {
+ val rw = RemoteWorkManager.getInstance(AngApplication.application)
+ rw.cancelUniqueWork(AppConfig.UPDATE_TASK_NAME)
+
private fun updateMux(enabled: Boolean) {
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
muxXudpConcurrency?.isEnabled = enabled
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt
index 124db66b..180b1200 100644
--- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt
@@ -5,14 +5,22 @@ import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequest
+import androidx.work.multiprocess.RemoteWorkManager
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
+import com.v2ray.ang.AngApplication
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubEditBinding
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.extension.toast
+import com.v2ray.ang.service.SubscriptionUpdater
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
+import java.util.concurrent.TimeUnit
class SubEditActivity : BaseActivity() {
private lateinit var binding: ActivitySubEditBinding
@@ -45,6 +53,7 @@ class SubEditActivity : BaseActivity() {
binding.etRemarks.text = Utils.getEditable(subItem.remarks)
binding.etUrl.text = Utils.getEditable(subItem.url)
binding.chkEnable.isChecked = subItem.enabled
+ binding.autoUpdateCheck.isChecked = subItem.autoUpdate
return true
}
@@ -75,6 +84,7 @@ class SubEditActivity : BaseActivity() {
subItem.remarks = binding.etRemarks.text.toString()
subItem.url = binding.etUrl.text.toString()
subItem.enabled = binding.chkEnable.isChecked
+ subItem.autoUpdate = binding.autoUpdateCheck.isChecked
if (TextUtils.isEmpty(subItem.remarks)) {
toast(R.string.sub_setting_remarks)
@@ -129,4 +139,5 @@ class SubEditActivity : BaseActivity() {
}
else -> super.onOptionsItemSelected(item)
}
+
}
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 770f93e4..d61169cf 100644
--- a/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml
+++ b/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml
@@ -94,6 +94,28 @@
+
+
+
+
+
+
+
+
+
remarks
Optional URL
enable update
+ Auto update
Update subscription
Tcping all configuration
Real delay all configuration
diff --git a/V2rayNG/app/src/main/res/xml/pref_settings.xml b/V2rayNG/app/src/main/res/xml/pref_settings.xml
index 2ab9ccf1..a14314d5 100644
--- a/V2rayNG/app/src/main/res/xml/pref_settings.xml
+++ b/V2rayNG/app/src/main/res/xml/pref_settings.xml
@@ -162,4 +162,42 @@
android:title="@string/title_mode" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+