diff --git a/V2rayNG/app/build.gradle b/V2rayNG/app/build.gradle index 663cbc2f..228e433d 100644 --- a/V2rayNG/app/build.gradle +++ b/V2rayNG/app/build.gradle @@ -132,4 +132,9 @@ dependencies { implementation 'com.blacksquircle.ui:language-json:2.1.1' 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/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 1dfa3b2b..fbefd94b 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt @@ -87,4 +87,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 5284cc64..c6da160c 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() @@ -31,12 +42,15 @@ 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) } + // 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) } @@ -51,6 +65,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) @@ -92,13 +124,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 -> @@ -125,14 +158,25 @@ 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, "") + 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) + 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) + autoUpdateInterval?.summary = defaultSharedPreferences.getString( + AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, + AppConfig.DEFAULT_UPDATE_INTERVAL + ) if (TextUtils.isEmpty(remoteDnsString)) { remoteDnsString = AppConfig.DNS_AGENT @@ -141,7 +185,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 @@ -155,17 +200,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 + ) + ) } } @@ -174,6 +226,33 @@ class SettingsActivity : BaseActivity() { localDnsPort?.isEnabled = enabled 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) + } } fun onModeHelpClicked(view: View) { 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 5a5a8d44..699f8de1 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 @@ -46,6 +54,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 } @@ -76,6 +85,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) @@ -130,4 +140,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 660f77c7..73d9ce51 100644 --- a/V2rayNG/app/src/main/res/xml/pref_settings.xml +++ b/V2rayNG/app/src/main/res/xml/pref_settings.xml @@ -126,6 +126,21 @@ + + + + + + + +