Compare commits

...

11 Commits

Author SHA1 Message Date
2dust d0265265f3 up 2.0.13 2026-03-03 16:11:10 +08:00
2dust 2d6dd33a7b Fix real ping test
https://github.com/2dust/v2rayNG/issues/5331
2026-03-01 17:19:42 +08:00
2dust 3a32373abe up 2.0.12 2026-02-28 15:10:40 +08:00
Hossein Abaspanah befb2937d1 Update Luri Bakhtiari translation (#5330) 2026-02-28 13:59:14 +08:00
solokot 0a375bec67 Update Russian translation (#5328) 2026-02-28 13:59:02 +08:00
2dust 029f8b67e7 Remove legacy profileStorage and migration code 2026-02-28 11:34:03 +08:00
2dust 7475d1757b Add detailed subscription update result 2026-02-27 17:44:40 +08:00
dependabot[bot] 5a16614a7b Bump actions/upload-artifact from 6 to 7 (#5324)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 14:18:55 +08:00
solokot edb06b466a Update Russian translation (#5315) 2026-02-26 14:56:53 +08:00
Hossein Abaspanah 30b5391895 Update strings.xml (#5312) 2026-02-26 14:56:41 +08:00
2dust 6cd04b831f Improve search
https://github.com/2dust/v2rayNG/issues/5313
2026-02-24 16:55:16 +08:00
22 changed files with 157 additions and 191 deletions
+3 -3
View File
@@ -99,21 +99,21 @@ jobs:
./gradlew assembleRelease -Pandroid.injected.signing.store.file=${{ steps.android_keystore.outputs.filePath }} -Pandroid.injected.signing.store.password=${{ secrets.APP_KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.APP_KEYSTORE_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.APP_KEY_PASSWORD }} ./gradlew assembleRelease -Pandroid.injected.signing.store.file=${{ steps.android_keystore.outputs.filePath }} -Pandroid.injected.signing.store.password=${{ secrets.APP_KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.APP_KEYSTORE_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.APP_KEY_PASSWORD }}
- name: Upload arm64-v8a APK - name: Upload arm64-v8a APK
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
if: ${{ success() }} if: ${{ success() }}
with: with:
name: arm64-v8a name: arm64-v8a
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*arm64-v8a*.apk path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*arm64-v8a*.apk
- name: Upload armeabi-v7a APK - name: Upload armeabi-v7a APK
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
if: ${{ success() }} if: ${{ success() }}
with: with:
name: armeabi-v7a name: armeabi-v7a
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*armeabi-v7a*.apk path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*armeabi-v7a*.apk
- name: Upload x86 APK - name: Upload x86 APK
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
if: ${{ success() }} if: ${{ success() }}
with: with:
name: x86-apk name: x86-apk
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang" applicationId = "com.v2ray.ang"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 711 versionCode = 713
versionName = "2.0.11" versionName = "2.0.13"
multiDexEnabled = true multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';') val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
@@ -77,7 +77,6 @@ object AppConfig {
/** Cache keys. */ /** Cache keys. */
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id" const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
/** Protocol identifiers. */ /** Protocol identifiers. */
const val PROTOCOL_FREEDOM = "freedom" const val PROTOCOL_FREEDOM = "freedom"
@@ -0,0 +1,24 @@
package com.v2ray.ang.dto
/**
* Result of subscription update operation
*/
data class SubscriptionUpdateResult(
val configCount: Int = 0, // Total configs updated
val successCount: Int = 0, // Subscriptions updated successfully
val failureCount: Int = 0, // Subscriptions failed to update
val skipCount: Int = 0 // Subscriptions skipped (disabled)
) {
/**
* Combine two results by adding their counts
*/
operator fun plus(other: SubscriptionUpdateResult): SubscriptionUpdateResult {
return SubscriptionUpdateResult(
configCount = this.configCount + other.configCount,
successCount = this.successCount + other.successCount,
failureCount = this.failureCount + other.failureCount,
skipCount = this.skipCount + other.skipCount
)
}
}
@@ -0,0 +1,10 @@
package com.v2ray.ang.dto
import java.io.Serializable
data class TestServiceMessage(
val key: Int,
val subscriptionId: String = "",
val serverGuids: List<String> = emptyList()
) : Serializable
@@ -10,6 +10,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.SubscriptionCache import com.v2ray.ang.dto.SubscriptionCache
import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.dto.SubscriptionUpdateResult
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.fmt.CustomFmt import com.v2ray.ang.fmt.CustomFmt
@@ -433,45 +434,48 @@ object AngConfigManager {
/** /**
* Updates the configuration via all subscriptions. * Updates the configuration via all subscriptions.
* *
* @return The number of configurations updated. * @return Detailed result of the subscription update operation.
*/ */
fun updateConfigViaSubAll(): Int { fun updateConfigViaSubAll(): SubscriptionUpdateResult {
var count = 0 return try {
try { val subscriptions = MmkvManager.decodeSubscriptions()
MmkvManager.decodeSubscriptions().forEach { subscriptions.fold(SubscriptionUpdateResult()) { acc, subscription ->
count += updateConfigViaSub(it) acc + updateConfigViaSub(subscription)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e) Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
return 0 SubscriptionUpdateResult()
} }
return count
} }
/** /**
* Updates the configuration via a subscription. * Updates the configuration via a subscription.
* *
* @param it The subscription item. * @param it The subscription item.
* @return The number of configurations updated. * @return Subscription update result.
*/ */
fun updateConfigViaSub(it: SubscriptionCache): Int { fun updateConfigViaSub(it: SubscriptionCache): SubscriptionUpdateResult {
try { try {
// Check if disabled
if (!it.subscription.enabled) {
return SubscriptionUpdateResult(skipCount = 1)
}
// Validate subscription info
if (TextUtils.isEmpty(it.guid) if (TextUtils.isEmpty(it.guid)
|| TextUtils.isEmpty(it.subscription.remarks) || TextUtils.isEmpty(it.subscription.remarks)
|| TextUtils.isEmpty(it.subscription.url) || TextUtils.isEmpty(it.subscription.url)
) { ) {
return 0 return SubscriptionUpdateResult(skipCount = 1)
}
if (!it.subscription.enabled) {
return 0
} }
val url = HttpUtil.toIdnUrl(it.subscription.url) val url = HttpUtil.toIdnUrl(it.subscription.url)
if (!Utils.isValidUrl(url)) { if (!Utils.isValidUrl(url)) {
return 0 return SubscriptionUpdateResult(failureCount = 1)
} }
if (!it.subscription.allowInsecureUrl) { if (!it.subscription.allowInsecureUrl) {
if (!Utils.isValidSubUrl(url)) { if (!Utils.isValidSubUrl(url)) {
return 0 return SubscriptionUpdateResult(failureCount = 1)
} }
} }
Log.i(AppConfig.TAG, url) Log.i(AppConfig.TAG, url)
@@ -493,18 +497,25 @@ object AngConfigManager {
} }
} }
if (configText.isEmpty()) { if (configText.isEmpty()) {
return 0 return SubscriptionUpdateResult(failureCount = 1)
} }
val count = parseConfigViaSub(configText, it.guid, false) val count = parseConfigViaSub(configText, it.guid, false)
if (count > 0) { if (count > 0) {
it.subscription.lastUpdated = System.currentTimeMillis() it.subscription.lastUpdated = System.currentTimeMillis()
MmkvManager.encodeSubscription(it.guid, it.subscription) MmkvManager.encodeSubscription(it.guid, it.subscription)
Log.i(AppConfig.TAG, "Subscription updated: ${it.subscription.remarks}, $count configs") Log.i(AppConfig.TAG, "Subscription updated: ${it.subscription.remarks}, $count configs")
return SubscriptionUpdateResult(
configCount = count,
successCount = 1
)
} else {
// Got response but no valid configs parsed
return SubscriptionUpdateResult(failureCount = 1)
} }
return count
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to update config via subscription", e) Log.e(AppConfig.TAG, "Failed to update config via subscription", e)
return 0 return SubscriptionUpdateResult(failureCount = 1)
} }
} }
@@ -19,7 +19,6 @@ object MmkvManager {
//region private //region private
//private const val ID_PROFILE_CONFIG = "PROFILE_CONFIG"
private const val ID_MAIN = "MAIN" private const val ID_MAIN = "MAIN"
private const val ID_PROFILE_FULL_CONFIG = "PROFILE_FULL_CONFIG" private const val ID_PROFILE_FULL_CONFIG = "PROFILE_FULL_CONFIG"
private const val ID_SERVER_RAW = "SERVER_RAW" private const val ID_SERVER_RAW = "SERVER_RAW"
@@ -33,7 +32,6 @@ object MmkvManager {
private const val KEY_SUB_IDS = "SUB_IDS" private const val KEY_SUB_IDS = "SUB_IDS"
private const val KEY_WEBDAV_CONFIG = "WEBDAV_CONFIG" private const val KEY_WEBDAV_CONFIG = "WEBDAV_CONFIG"
//private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) }
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) } private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
private val profileFullStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_FULL_CONFIG, MMKV.MULTI_PROCESS_MODE) } private val profileFullStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_FULL_CONFIG, MMKV.MULTI_PROCESS_MODE) }
private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) } private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
@@ -56,13 +54,6 @@ object MmkvManager {
return mainStorage.decodeString(KEY_ANG_CONFIGS) return mainStorage.decodeString(KEY_ANG_CONFIGS)
} }
// /**
// * Removes the legacy KEY_ANG_CONFIGS after migration.
// * This method is for migration purposes only.
// */
// fun removeLegacyServerListKey() {
// mainStorage.remove(KEY_ANG_CONFIGS)
// }
/** /**
* Gets the selected server GUID. * Gets the selected server GUID.
@@ -95,13 +86,6 @@ object MmkvManager {
mainStorage.encode(key, JsonUtil.toJson(serverList)) mainStorage.encode(key, JsonUtil.toJson(serverList))
} }
// Legacy method for compatibility
// TODO: Remove after migration and update all callers
/*
fun encodeServerList(serverList: MutableList<String>) {
mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
}
*/
/** /**
* Decodes the server list for a given subscription. * Decodes the server list for a given subscription.
@@ -139,18 +123,6 @@ object MmkvManager {
return allServers return allServers
} }
// Legacy method for compatibility - reads all servers
// TODO: Remove after migration and update all callers
/*
fun decodeServerList(): MutableList<String> {
val json = mainStorage.decodeString(KEY_ANG_CONFIGS)
return if (json.isNullOrBlank()) {
mutableListOf()
} else {
JsonUtil.fromJson(json, Array<String>::class.java)?.toMutableList() ?: mutableListOf()
}
}
*/
/** /**
* Decodes the server configuration. * Decodes the server configuration.
@@ -169,16 +141,6 @@ object MmkvManager {
return JsonUtil.fromJson(json, ProfileItem::class.java) return JsonUtil.fromJson(json, ProfileItem::class.java)
} }
// fun decodeProfileConfig(guid: String): ProfileLiteItem? {
// if (guid.isBlank()) {
// return null
// }
// val json = profileStorage.decodeString(guid)
// if (json.isNullOrBlank()) {
// return null
// }
// return JsonUtil.fromJson(json, ProfileLiteItem::class.java)
// }
/** /**
* Encodes the server configuration. * Encodes the server configuration.
@@ -203,25 +165,6 @@ object MmkvManager {
} }
} }
// Legacy code - keep for reference during migration
/*
val serverList = decodeServerList()
if (!serverList.contains(key)) {
serverList.add(0, key)
encodeServerList(serverList)
if (getSelectServer().isNullOrBlank()) {
mainStorage.encode(KEY_SELECTED_SERVER, key)
}
}
*/
// val profile = ProfileLiteItem(
// configType = config.configType,
// subscriptionId = config.subscriptionId,
// remarks = config.remarks,
// server = config.getProxyOutbound()?.getServerAddress(),
// serverPort = config.getProxyOutbound()?.getServerPort(),
// )
// profileStorage.encode(key, JsonUtil.toJson(profile))
return key return key
} }
@@ -254,22 +197,11 @@ object MmkvManager {
serverList.remove(guid) serverList.remove(guid)
encodeServerList(serverList, subId) encodeServerList(serverList, subId)
// Legacy code - keep for reference during migration
/*
if (getSelectServer() == guid) {
mainStorage.remove(KEY_SELECTED_SERVER)
}
val serverList = decodeServerList()
serverList.remove(guid)
encodeServerList(serverList)
*/
// Clean up storage // Clean up storage
if (getSelectServer() == guid) { if (getSelectServer() == guid) {
mainStorage.remove(KEY_SELECTED_SERVER) mainStorage.remove(KEY_SELECTED_SERVER)
} }
profileFullStorage.remove(guid) profileFullStorage.remove(guid)
//profileStorage.remove(guid)
serverAffStorage.remove(guid) serverAffStorage.remove(guid)
} }
@@ -293,17 +225,6 @@ object MmkvManager {
serverList.clear() serverList.clear()
encodeServerList(serverList, subId) encodeServerList(serverList, subId)
// Legacy code - keep for reference during migration
/*
profileFullStorage.allKeys()?.forEach { key ->
decodeServerConfig(key)?.let { config ->
if (config.subscriptionId == subid) {
removeServer(key)
}
}
}
*/
} }
/** /**
@@ -361,7 +282,6 @@ object MmkvManager {
val count = profileFullStorage.allKeys()?.count() ?: 0 val count = profileFullStorage.allKeys()?.count() ?: 0
mainStorage.clearAll() mainStorage.clearAll()
profileFullStorage.clearAll() profileFullStorage.clearAll()
//profileStorage.clearAll()
serverAffStorage.clearAll() serverAffStorage.clearAll()
return count return count
} }
@@ -502,8 +502,6 @@ object SettingsManager {
MmkvManager.encodeServerList(serverGuids, subId) MmkvManager.encodeServerList(serverGuids, subId)
} }
// Remove legacy KEY_ANG_CONFIGS data
// MmkvManager.removeLegacyServerListKey()
// Mark migration as complete // Mark migration as complete
MmkvManager.encodeSettings(migrationKey, true) MmkvManager.encodeSettings(migrationKey, true)
@@ -6,6 +6,8 @@ import android.os.IBinder
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
import com.v2ray.ang.dto.TestServiceMessage
import com.v2ray.ang.extension.serializable
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2RayNativeManager import com.v2ray.ang.handler.V2RayNativeManager
import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MessageUtil
@@ -52,13 +54,15 @@ class V2RayTestService : Service() {
* @return The start mode. * @return The start mode.
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.getIntExtra("key", 0)) { val message = intent?.serializable<TestServiceMessage>("content") ?: return super.onStartCommand(intent, flags, startId)
when (message.key) {
MSG_MEASURE_CONFIG -> { MSG_MEASURE_CONFIG -> {
val subscriptionId = intent.getStringExtra("content").orEmpty() val guidsList = if (message.serverGuids.isNotEmpty()) {
val guidsList = if (subscriptionId.isEmpty()) { message.serverGuids
MmkvManager.decodeAllServerList() } else if (message.subscriptionId.isNotEmpty()) {
MmkvManager.decodeServerList(message.subscriptionId)
} else { } else {
MmkvManager.decodeServerList(subscriptionId) MmkvManager.decodeAllServerList()
} }
if (guidsList.isNotEmpty()) { if (guidsList.isNotEmpty()) {
@@ -437,14 +437,23 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
showLoading() showLoading()
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val count = mainViewModel.updateConfigViaSubAll() val result = mainViewModel.updateConfigViaSubAll()
delay(500L) delay(500L)
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
if (count > 0) { if (result.successCount + result.failureCount + result.skipCount == 0) {
toast(getString(R.string.title_update_config_count, count)) toast(R.string.title_update_subscription_no_subscription)
mainViewModel.reloadServerList() } else if (result.successCount > 0 && result.failureCount + result.skipCount == 0) {
toast(getString(R.string.title_update_config_count, result.configCount))
} else { } else {
toastError(R.string.toast_failure) toast(
getString(
R.string.title_update_subscription_result,
result.configCount, result.successCount, result.failureCount, result.skipCount
)
)
}
if (result.configCount > 0) {
mainViewModel.reloadServerList()
} }
hideLoading() hideLoading()
} }
@@ -18,8 +18,6 @@ import com.v2ray.ang.contracts.BaseAdapterListener
import com.v2ray.ang.databinding.ActivitySubSettingBinding import com.v2ray.ang.databinding.ActivitySubSettingBinding
import com.v2ray.ang.databinding.ItemQrcodeBinding import com.v2ray.ang.databinding.ItemQrcodeBinding
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
@@ -77,14 +75,20 @@ class SubSettingActivity : BaseActivity() {
showLoading() showLoading()
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val count = AngConfigManager.updateConfigViaSubAll() val result = AngConfigManager.updateConfigViaSubAll()
delay(500L) delay(500L)
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
if (count > 0) { if (result.successCount + result.failureCount + result.skipCount == 0) {
toastSuccess(R.string.toast_success) toast(R.string.title_update_subscription_no_subscription)
refreshData() } else if (result.successCount > 0 && result.failureCount + result.skipCount == 0) {
toast(getString(R.string.title_update_config_count, result.configCount))
} else { } else {
toastError(R.string.toast_failure) toast(
getString(
R.string.title_update_subscription_result,
result.configCount, result.successCount, result.failureCount, result.skipCount
)
)
} }
hideLoading() hideLoading()
} }
@@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.TestServiceMessage
import com.v2ray.ang.service.V2RayTestService import com.v2ray.ang.service.V2RayTestService
import java.io.Serializable import java.io.Serializable
@@ -37,15 +38,13 @@ object MessageUtil {
* Sends a message to the test service. * Sends a message to the test service.
* *
* @param ctx The context. * @param ctx The context.
* @param what The message identifier. * @param message The test service message containing key, subscriptionId, and serverGuids.
* @param content The message content.
*/ */
fun sendMsg2TestService(ctx: Context, what: Int, content: Serializable) { fun sendMsg2TestService(ctx: Context, message: TestServiceMessage) {
try { try {
val intent = Intent() val intent = Intent()
intent.component = ComponentName(ctx, V2RayTestService::class.java) intent.component = ComponentName(ctx, V2RayTestService::class.java)
intent.putExtra("key", what) intent.putExtra("content", message)
intent.putExtra("content", content)
ctx.startService(intent) ctx.startService(intent)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to send message to test service", e) Log.e(AppConfig.TAG, "Failed to send message to test service", e)
@@ -17,6 +17,8 @@ import com.v2ray.ang.R
import com.v2ray.ang.dto.GroupMapItem import com.v2ray.ang.dto.GroupMapItem
import com.v2ray.ang.dto.ServersCache import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.dto.SubscriptionCache import com.v2ray.ang.dto.SubscriptionCache
import com.v2ray.ang.dto.SubscriptionUpdateResult
import com.v2ray.ang.dto.TestServiceMessage
import com.v2ray.ang.extension.serializable import com.v2ray.ang.extension.serializable
import com.v2ray.ang.extension.toastError import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
@@ -37,8 +39,6 @@ import java.util.Collections
class MainViewModel(application: Application) : AndroidViewModel(application) { class MainViewModel(application: Application) : AndroidViewModel(application) {
private var serverList = mutableListOf<String>() // MmkvManager.decodeServerList() private var serverList = mutableListOf<String>() // MmkvManager.decodeServerList()
var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty() var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
//var keywordFilter: String = MmkvManager.MmkvManager.decodeSettingsString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
var keywordFilter = "" var keywordFilter = ""
val serversCache = mutableListOf<ServersCache>() val serversCache = mutableListOf<ServersCache>()
val isRunning by lazy { MutableLiveData<Boolean>() } val isRunning by lazy { MutableLiveData<Boolean>() }
@@ -95,38 +95,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
} }
// /**
// * Appends a custom configuration server.
// * @param server The server configuration to append.
// * @return True if the server was successfully appended, false otherwise.
// */
// fun appendCustomConfigServer(server: String): Boolean {
// if (server.contains("inbounds")
// && server.contains("outbounds")
// && server.contains("routing")
// ) {
// try {
// val config = CustomFmt.parse(server) ?: return false
// config.subscriptionId = subscriptionId
// val key = MmkvManager.encodeServerConfig("", config)
// MmkvManager.encodeServerRaw(key, server)
// serverList.add(0, key)
//// val profile = ProfileLiteItem(
//// configType = config.configType,
//// subscriptionId = config.subscriptionId,
//// remarks = config.remarks,
//// server = config.getProxyOutbound()?.getServerAddress(),
//// serverPort = config.getProxyOutbound()?.getServerPort(),
//// )
// serversCache.add(0, ServersCache(key, config))
// return true
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
// return false
// }
/** /**
* Swaps the positions of two servers. * Swaps the positions of two servers.
* @param fromPosition The initial position of the server. * @param fromPosition The initial position of the server.
@@ -149,26 +117,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
@Synchronized @Synchronized
fun updateCache() { fun updateCache() {
serversCache.clear() serversCache.clear()
val kw = keywordFilter.trim().lowercase()
for (guid in serverList) { for (guid in serverList) {
val profile = MmkvManager.decodeServerConfig(guid) ?: continue val profile = MmkvManager.decodeServerConfig(guid) ?: continue
// var profile = MmkvManager.decodeProfileConfig(guid) if (kw.isEmpty()) {
// if (profile == null) { serversCache.add(ServersCache(guid, profile))
// val config = MmkvManager.decodeServerConfig(guid) ?: continue continue
// profile = ProfileLiteItem( }
// configType = config.configType,
// subscriptionId = config.subscriptionId,
// remarks = config.remarks,
// server = config.getProxyOutbound()?.getServerAddress(),
// serverPort = config.getProxyOutbound()?.getServerPort(),
// )
// MmkvManager.encodeServerConfig(guid, config)
// }
// if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) { val remarks = profile.remarks.lowercase()
// continue val description = profile.description.orEmpty().lowercase()
// } val server = profile.server.orEmpty().lowercase()
if (keywordFilter.isEmpty() || profile.remarks.lowercase().contains(keywordFilter.lowercase())) { if (remarks.contains(kw) || description.contains(kw) || server.contains(kw)) {
serversCache.add(ServersCache(guid, profile)) serversCache.add(ServersCache(guid, profile))
} }
} }
@@ -176,13 +137,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
/** /**
* Updates the configuration via subscription for all servers. * Updates the configuration via subscription for all servers.
* @return The number of updated configurations. * @return Detailed result of the subscription update operation.
*/ */
fun updateConfigViaSubAll(): Int { fun updateConfigViaSubAll(): SubscriptionUpdateResult {
if (subscriptionId.isEmpty()) { if (subscriptionId.isEmpty()) {
return AngConfigManager.updateConfigViaSubAll() return AngConfigManager.updateConfigViaSubAll()
} else { } else {
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return 0 val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return SubscriptionUpdateResult()
return AngConfigManager.updateConfigViaSub(SubscriptionCache(subscriptionId, subItem)) return AngConfigManager.updateConfigViaSub(SubscriptionCache(subscriptionId, subItem))
} }
} }
@@ -236,7 +197,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
* Tests the real ping for all servers. * Tests the real ping for all servers.
*/ */
fun testAllRealPing() { fun testAllRealPing() {
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "") MessageUtil.sendMsg2TestService(
getApplication(),
TestServiceMessage(key = AppConfig.MSG_MEASURE_CONFIG_CANCEL)
)
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList()) MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
updateListAction.value = -1 updateListAction.value = -1
@@ -244,7 +208,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
if (serversCache.isEmpty()) { if (serversCache.isEmpty()) {
return@launch return@launch
} }
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, subscriptionId) MessageUtil.sendMsg2TestService(
getApplication(),
TestServiceMessage(
key = AppConfig.MSG_MEASURE_CONFIG,
subscriptionId = subscriptionId,
serverGuids = if (keywordFilter.isNotEmpty()) serversCache.map { it.guid } else emptyList()
)
)
} }
} }
@@ -430,7 +401,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return return
} }
keywordFilter = keyword keywordFilter = keyword
MmkvManager.encodeSettings(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
reloadServerList() reloadServerList()
} }
@@ -297,6 +297,8 @@
<string name="title_import_config_count">Import %d configurations</string> <string name="title_import_config_count">Import %d configurations</string>
<string name="title_export_config_count">Export %d configurations</string> <string name="title_export_config_count">Export %d configurations</string>
<string name="title_update_config_count">Update %d configurations</string> <string name="title_update_config_count">Update %d configurations</string>
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
<string name="title_update_subscription_no_subscription">No subscriptions</string>
<string name="tasker_start_service">بدء الخدمة</string> <string name="tasker_start_service">بدء الخدمة</string>
<string name="tasker_setting_confirm">تأكيد</string> <string name="tasker_setting_confirm">تأكيد</string>
@@ -296,6 +296,8 @@
<string name="title_import_config_count">Import %d configurations</string> <string name="title_import_config_count">Import %d configurations</string>
<string name="title_export_config_count">Export %d configurations</string> <string name="title_export_config_count">Export %d configurations</string>
<string name="title_update_config_count">Update %d configurations</string> <string name="title_update_config_count">Update %d configurations</string>
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
<string name="title_update_subscription_no_subscription">No subscriptions</string>
<string name="tasker_start_service">সার্ভিস শুরু করুন</string> <string name="tasker_start_service">সার্ভিস শুরু করুন</string>
<string name="tasker_setting_confirm">নিশ্চিত করুন</string> <string name="tasker_setting_confirm">নিশ্চিত করুন</string>
@@ -234,8 +234,8 @@
<string name="title_pref_double_column_display">ره وندن نشووݩ داڌن دو سۊتۊنی</string> <string name="title_pref_double_column_display">ره وندن نشووݩ داڌن دو سۊتۊنی</string>
<string name="summary_pref_double_column_display">نومگه نمایه یل من دو سۊتۊن نشووݩ داڌه ابۊن وو چینۉ ترین موئتوا بیشتری ن سیل کۊنین. سی ره وستن، وا برنومه ن ز نۊ ره ونین.</string> <string name="summary_pref_double_column_display">نومگه نمایه یل من دو سۊتۊن نشووݩ داڌه ابۊن وو چینۉ ترین موئتوا بیشتری ن سیل کۊنین. سی ره وستن، وا برنومه ن ز نۊ ره ونین.</string>
<string name="title_pref_group_all_display">Enable show all groups</string> <string name="title_pref_group_all_display">نشووݩ داڌن پوی بونکۊ یل ن فعال کۊنین</string>
<string name="summary_pref_group_all_display">Add an extra "All Tabs" page</string> <string name="summary_pref_group_all_display">ی بلگه «پوی بلگه یل» ازاف کۊنین</string>
<!-- AboutActivity --> <!-- AboutActivity -->
<string name="title_pref_feedback">فشناڌن منشڌ</string> <string name="title_pref_feedback">فشناڌن منشڌ</string>
<string name="summary_pref_feedback">فشناڌن منشڌ یا گوزارش موشکلا من Github</string> <string name="summary_pref_feedback">فشناڌن منشڌ یا گوزارش موشکلا من Github</string>
@@ -297,6 +297,8 @@
<string name="title_import_config_count">و من ٱووردن %d کانفیگ</string> <string name="title_import_config_count">و من ٱووردن %d کانفیگ</string>
<string name="title_export_config_count">و در کشیڌن %d کانفیگ</string> <string name="title_export_config_count">و در کشیڌن %d کانفیگ</string>
<string name="title_update_config_count">ورۊ کردن %d کانفیگ</string> <string name="title_update_config_count">ورۊ کردن %d کانفیگ</string>
<string name="title_update_subscription_result">%1$d کانفیگ ورۊ وابی (مووفق: %2$d، شکست خرده: %3$d، رڌ وابیڌه: %4$d)</string>
<string name="title_update_subscription_no_subscription">بؽ اشتراک</string>
<string name="tasker_start_service">ره وندن خدمات</string> <string name="tasker_start_service">ره وندن خدمات</string>
<string name="tasker_setting_confirm">قوۊل</string> <string name="tasker_setting_confirm">قوۊل</string>
@@ -417,7 +419,7 @@
<string-array name="policy_group_type"> <string-array name="policy_group_type">
<item>کم ترین پینگ</item> <item>کم ترین پینگ</item>
<item>کم ترین بار(لود)</item> <item>کم ترین بار(لود)</item>
<item>تصادفی</item> <item>تساڌۊفی</item>
<item>گردشی</item> <item>گردشی</item>
</string-array> </string-array>
@@ -294,6 +294,8 @@
<string name="title_import_config_count">وارد کردن %d کانفیگ</string> <string name="title_import_config_count">وارد کردن %d کانفیگ</string>
<string name="title_export_config_count">صادر کردن %d کانفیگ</string> <string name="title_export_config_count">صادر کردن %d کانفیگ</string>
<string name="title_update_config_count">آپدیت کردن %d کانفیگ</string> <string name="title_update_config_count">آپدیت کردن %d کانفیگ</string>
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
<string name="title_update_subscription_no_subscription">No subscriptions</string>
<string name="tasker_start_service">شروع خدمات</string> <string name="tasker_start_service">شروع خدمات</string>
<string name="tasker_setting_confirm">تایید</string> <string name="tasker_setting_confirm">تایید</string>
@@ -233,8 +233,8 @@
<string name="title_pref_double_column_display">Профили в два столбца</string> <string name="title_pref_double_column_display">Профили в два столбца</string>
<string name="summary_pref_double_column_display">Список профилей отображается двумя столбцами, что позволяет показать больше информации на экране. Требуется перезапуск приложения.</string> <string name="summary_pref_double_column_display">Список профилей отображается двумя столбцами, что позволяет показать больше информации на экране. Требуется перезапуск приложения.</string>
<string name="title_pref_group_all_display">Enable show all groups</string> <string name="title_pref_group_all_display">Общая вкладка групп</string>
<string name="summary_pref_group_all_display">Add an extra "All Tabs" page</string> <string name="summary_pref_group_all_display">Показывать дополнительную вкладку со всеми профилями групп</string>
<!-- AboutActivity --> <!-- AboutActivity -->
<string name="title_pref_feedback">Обратная связь</string> <string name="title_pref_feedback">Обратная связь</string>
<string name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</string> <string name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</string>
@@ -295,6 +295,8 @@
<string name="title_import_config_count">Импортировано профилей: %d</string> <string name="title_import_config_count">Импортировано профилей: %d</string>
<string name="title_export_config_count">Экспортировано профилей: %d</string> <string name="title_export_config_count">Экспортировано профилей: %d</string>
<string name="title_update_config_count">Обновлено профилей: %d</string> <string name="title_update_config_count">Обновлено профилей: %d</string>
<string name="title_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string>
<string name="title_update_subscription_no_subscription">Нет подписок</string>
<string name="tasker_start_service">Запуск службы</string> <string name="tasker_start_service">Запуск службы</string>
<string name="tasker_setting_confirm">Подтвердить</string> <string name="tasker_setting_confirm">Подтвердить</string>
@@ -358,7 +360,7 @@
<string name="title_configuration_restore">Восстановление конфигурации</string> <string name="title_configuration_restore">Восстановление конфигурации</string>
<string name="title_configuration_share">Поделиться конфигурацией</string> <string name="title_configuration_share">Поделиться конфигурацией</string>
<string name="title_webdav_config_setting">Настройки WebDAV</string> <string name="title_webdav_config_setting">Настройки WebDAV</string>
<string name="title_webdav_config_setting_unknown">Необходимо настроить WebDAV.</string> <string name="title_webdav_config_setting_unknown">Необходимо настроить WebDAV</string>
<string name="title_webdav_url">URL сервера</string> <string name="title_webdav_url">URL сервера</string>
<string name="title_webdav_user">Пользователь</string> <string name="title_webdav_user">Пользователь</string>
<string name="title_webdav_pass">Пароль</string> <string name="title_webdav_pass">Пароль</string>
@@ -297,6 +297,8 @@
<string name="title_import_config_count">Import %d configurations</string> <string name="title_import_config_count">Import %d configurations</string>
<string name="title_export_config_count">Export %d configurations</string> <string name="title_export_config_count">Export %d configurations</string>
<string name="title_update_config_count">Update %d configurations</string> <string name="title_update_config_count">Update %d configurations</string>
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
<string name="title_update_subscription_no_subscription">No subscriptions</string>
<string name="tasker_start_service">Khởi động v2rayNG</string> <string name="tasker_start_service">Khởi động v2rayNG</string>
<string name="tasker_setting_confirm">Xác nhận</string> <string name="tasker_setting_confirm">Xác nhận</string>
@@ -295,6 +295,8 @@
<string name="title_import_config_count">导入 %d 个配置</string> <string name="title_import_config_count">导入 %d 个配置</string>
<string name="title_export_config_count">导出 %d 个配置</string> <string name="title_export_config_count">导出 %d 个配置</string>
<string name="title_update_config_count">更新 %d 个配置</string> <string name="title_update_config_count">更新 %d 个配置</string>
<string name="title_update_subscription_result">更新了 %1$d 个配置(%2$d 个成功,%3$d 个失败,%4$d 个跳过)</string>
<string name="title_update_subscription_no_subscription">无订阅</string>
<string name="tasker_start_service">启动服务</string> <string name="tasker_start_service">启动服务</string>
<string name="tasker_setting_confirm">确定</string> <string name="tasker_setting_confirm">确定</string>
@@ -295,6 +295,8 @@
<string name="title_import_config_count">匯入 %d 個配置</string> <string name="title_import_config_count">匯入 %d 個配置</string>
<string name="title_export_config_count">匯出 %d 個配置</string> <string name="title_export_config_count">匯出 %d 個配置</string>
<string name="title_update_config_count">更新 %d 個配置</string> <string name="title_update_config_count">更新 %d 個配置</string>
<string name="title_update_subscription_result">更新了 %1$d 個配置(%2$d 個成功,%3$d 個失敗,%4$d 個跳過)</string>
<string name="title_update_subscription_no_subscription">無訂閱</string>
<string name="tasker_start_service">啟動服務</string> <string name="tasker_start_service">啟動服務</string>
<string name="tasker_setting_confirm">確定</string> <string name="tasker_setting_confirm">確定</string>
@@ -301,6 +301,8 @@
<string name="title_import_config_count">Import %d configs</string> <string name="title_import_config_count">Import %d configs</string>
<string name="title_export_config_count">Export %d configs</string> <string name="title_export_config_count">Export %d configs</string>
<string name="title_update_config_count">Update %d configs</string> <string name="title_update_config_count">Update %d configs</string>
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
<string name="title_update_subscription_no_subscription">No subscriptions</string>
<string name="tasker_start_service">Start Service</string> <string name="tasker_start_service">Start Service</string>
<string name="tasker_setting_confirm">Confirm</string> <string name="tasker_setting_confirm">Confirm</string>