Compare commits

..

18 Commits

Author SHA1 Message Date
2dust 80eec036dd up 2.0.14 2026-03-10 19:22:50 +08:00
2dust f9a85366ea Update libs.versions.toml 2026-03-10 17:55:48 +08:00
2dust 378c891399 Update submodule commit SHAs 2026-03-09 20:37:30 +08:00
2dust d04b5eee37 Add detailed logging and validations to services 2026-03-09 20:11:53 +08:00
2dust 405cd7f55e Ensure default subscription on removals
Add removeSubscriptionWithDefault in SettingsManager to remove a subscription and recreate a default subscription if none remain. Modify MmkvManager.decodeAllServerList to include servers from DEFAULT_SUBSCRIPTION_ID when it's not listed, and remove the defensive check in MmkvManager.removeSubscription so removals are delegated to SettingsManager. Update callers (SubEditActivity, SubscriptionsViewModel) to use SettingsManager.removeSubscriptionWithDefault, remove UI restrictions hiding the delete action for the default subscription, and simplify group-display logic in MainViewModel. These changes ensure a default subscription always exists and server lists include default servers when appropriate.
2026-03-09 19:38:29 +08:00
2dust 850096789b When geoip:cn and geoip:private appear in the routing rules, load geoip-only-cn-private.dat to reduce memory usage. 2026-03-09 16:52:57 +08:00
2dust 3d42ac9dba Add logging and safety checks in boot and VPN service
https://github.com/2dust/v2rayNG/issues/5346
2026-03-08 16:11:33 +08:00
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
34 changed files with 353 additions and 256 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 }}
- name: Upload arm64-v8a APK
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
if: ${{ success() }}
with:
name: arm64-v8a
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*arm64-v8a*.apk
- name: Upload armeabi-v7a APK
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
if: ${{ success() }}
with:
name: armeabi-v7a
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*armeabi-v7a*.apk
- name: Upload x86 APK
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
if: ${{ success() }}
with:
name: x86-apk
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 24
targetSdk = 36
versionCode = 711
versionName = "2.0.11"
versionCode = 714
versionName = "2.0.14"
multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
@@ -77,7 +77,6 @@ object AppConfig {
/** Cache keys. */
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
/** Protocol identifiers. */
const val PROTOCOL_FREEDOM = "freedom"
@@ -133,6 +132,12 @@ object AppConfig {
const val GEOIP_PRIVATE = "geoip:private"
const val GEOIP_CN = "geoip:cn"
/** Geo data file names. */
const val GEOSITE_DAT = "geosite.dat"
const val GEOIP_DAT = "geoip.dat"
const val GEOIP_ONLY_CN_PRIVATE_DAT = "geoip-only-cn-private.dat"
const val GEOIP_ONLY_CN_PRIVATE_URL = "$GITHUB_RAW_URL/Loyalsoldier/geoip/release/$GEOIP_ONLY_CN_PRIVATE_DAT"
/** Ports and addresses for various services. */
const val PORT_LOCAL_DNS = "10853"
const val PORT_SOCKS = "10808"
@@ -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.SubscriptionCache
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.dto.SubscriptionUpdateResult
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.fmt.CustomFmt
@@ -433,45 +434,48 @@ object AngConfigManager {
/**
* Updates the configuration via all subscriptions.
*
* @return The number of configurations updated.
* @return Detailed result of the subscription update operation.
*/
fun updateConfigViaSubAll(): Int {
var count = 0
try {
MmkvManager.decodeSubscriptions().forEach {
count += updateConfigViaSub(it)
fun updateConfigViaSubAll(): SubscriptionUpdateResult {
return try {
val subscriptions = MmkvManager.decodeSubscriptions()
subscriptions.fold(SubscriptionUpdateResult()) { acc, subscription ->
acc + updateConfigViaSub(subscription)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
return 0
SubscriptionUpdateResult()
}
return count
}
/**
* Updates the configuration via a subscription.
*
* @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 {
// Check if disabled
if (!it.subscription.enabled) {
return SubscriptionUpdateResult(skipCount = 1)
}
// Validate subscription info
if (TextUtils.isEmpty(it.guid)
|| TextUtils.isEmpty(it.subscription.remarks)
|| TextUtils.isEmpty(it.subscription.url)
) {
return 0
}
if (!it.subscription.enabled) {
return 0
return SubscriptionUpdateResult(skipCount = 1)
}
val url = HttpUtil.toIdnUrl(it.subscription.url)
if (!Utils.isValidUrl(url)) {
return 0
return SubscriptionUpdateResult(failureCount = 1)
}
if (!it.subscription.allowInsecureUrl) {
if (!Utils.isValidSubUrl(url)) {
return 0
return SubscriptionUpdateResult(failureCount = 1)
}
}
Log.i(AppConfig.TAG, url)
@@ -493,18 +497,25 @@ object AngConfigManager {
}
}
if (configText.isEmpty()) {
return 0
return SubscriptionUpdateResult(failureCount = 1)
}
val count = parseConfigViaSub(configText, it.guid, false)
if (count > 0) {
it.subscription.lastUpdated = System.currentTimeMillis()
MmkvManager.encodeSubscription(it.guid, it.subscription)
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) {
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
//private const val ID_PROFILE_CONFIG = "PROFILE_CONFIG"
private const val ID_MAIN = "MAIN"
private const val ID_PROFILE_FULL_CONFIG = "PROFILE_FULL_CONFIG"
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_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 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) }
@@ -56,13 +54,6 @@ object MmkvManager {
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.
@@ -95,13 +86,6 @@ object MmkvManager {
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.
@@ -130,27 +114,21 @@ object MmkvManager {
*/
fun decodeAllServerList(): MutableList<String> {
val allServers = mutableListOf<String>()
val subsList = decodeSubsList()
// Add servers from all subscriptions (including default subscription)
decodeSubsList().forEach { guid ->
// If DEFAULT_SUBSCRIPTION_ID is not in the subscriptions list, add its servers
if (!subsList.contains(DEFAULT_SUBSCRIPTION_ID)) {
allServers.addAll(decodeServerList(DEFAULT_SUBSCRIPTION_ID))
}
// Add servers from all subscriptions
subsList.forEach { guid ->
allServers.addAll(decodeServerList(guid))
}
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.
@@ -169,16 +147,6 @@ object MmkvManager {
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.
@@ -203,25 +171,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
}
@@ -254,22 +203,11 @@ object MmkvManager {
serverList.remove(guid)
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
if (getSelectServer() == guid) {
mainStorage.remove(KEY_SELECTED_SERVER)
}
profileFullStorage.remove(guid)
//profileStorage.remove(guid)
serverAffStorage.remove(guid)
}
@@ -293,17 +231,6 @@ object MmkvManager {
serverList.clear()
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 +288,6 @@ object MmkvManager {
val count = profileFullStorage.allKeys()?.count() ?: 0
mainStorage.clearAll()
profileFullStorage.clearAll()
//profileStorage.clearAll()
serverAffStorage.clearAll()
return count
}
@@ -461,11 +387,6 @@ object MmkvManager {
* @param subid The subscription ID.
*/
fun removeSubscription(subid: String) {
// Protect default subscription from being deleted
if (subid == DEFAULT_SUBSCRIPTION_ID) {
return
}
subStorage.remove(subid)
val subsList = decodeSubsList()
subsList.remove(subid)
@@ -25,6 +25,7 @@ import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
import com.v2ray.ang.handler.MmkvManager.decodeSubsList
import com.v2ray.ang.handler.MmkvManager.decodeSubscription
import com.v2ray.ang.handler.MmkvManager.encodeSubscription
import com.v2ray.ang.handler.MmkvManager.removeSubscription
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
import java.io.File
@@ -36,7 +37,7 @@ object SettingsManager {
fun initApp(context: Context) {
ensureDefaultSettings()
ensureDefaultSubscription()
//ensureDefaultSubscription()
initRoutingRulesets(context)
migrateServerListToSubscriptions()
migrateHysteria2PinSHA256()
@@ -240,6 +241,33 @@ object SettingsManager {
.firstOrNull { it.remarks == remarks }
}
/**
* Removes the subscription.
* If there are no remaining subscriptions,
* 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) {
// Log.i(ANG_PACKAGE,"Attempted to remove the only existing default subscription, operation ignored.")
// return
// }
// Remove the subscription
removeSubscription(subid)
// After removal, check if there are any subscriptions left. If not, create a default subscription.
val subsList2 = decodeSubsList()
if (subsList2.isNotEmpty()) {
return
}
val defaultSub = SubscriptionItem(
remarks = "Default",
)
encodeSubscription(DEFAULT_SUBSCRIPTION_ID, defaultSub)
}
/**
* Get the SOCKS port.
* @return The SOCKS port.
@@ -265,7 +293,7 @@ object SettingsManager {
val extFolder = Utils.userAssetPath(context)
try {
val geo = arrayOf("geosite.dat", "geoip.dat")
val geo = arrayOf(AppConfig.GEOSITE_DAT, AppConfig.GEOIP_DAT, AppConfig.GEOIP_ONLY_CN_PRIVATE_DAT)
assets.list("")
?.filter { geo.contains(it) }
?.filter { !File(extFolder, it).exists() }
@@ -502,8 +530,6 @@ object SettingsManager {
MmkvManager.encodeServerList(serverGuids, subId)
}
// Remove legacy KEY_ANG_CONFIGS data
// MmkvManager.removeLegacyServerListKey()
// Mark migration as complete
MmkvManager.encodeSettings(migrationKey, true)
@@ -57,9 +57,12 @@ object V2RayServiceManager {
* @param guid The GUID of the server configuration to use (optional).
*/
fun startVService(context: Context, guid: String? = null) {
Log.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}")
if (guid != null) {
MmkvManager.setSelectServer(guid)
}
startContextService(context)
}
@@ -91,15 +94,30 @@ object V2RayServiceManager {
*/
private fun startContextService(context: Context) {
if (coreController.isRunning) {
Log.w(AppConfig.TAG, "StartCore-Manager: Core already running")
return
}
val guid = MmkvManager.getSelectServer() ?: return
val config = MmkvManager.decodeServerConfig(guid) ?: return
val guid = MmkvManager.getSelectServer()
if (guid == null) {
Log.e(AppConfig.TAG, "StartCore-Manager: No server selected")
return
}
val config = MmkvManager.decodeServerConfig(guid)
if (config == null) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
return
}
if (config.configType != EConfigType.CUSTOM
&& config.configType != EConfigType.POLICYGROUP
&& !Utils.isValidUrl(config.server)
&& !Utils.isPureIpAddress(config.server.orEmpty())
) return
) {
Log.e(AppConfig.TAG, "StartCore-Manager: Invalid server configuration")
return
}
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
// if (!result.status) return
@@ -108,12 +126,21 @@ object V2RayServiceManager {
} else {
context.toast(R.string.toast_services_start)
}
val intent = if (SettingsManager.isVpnMode()) {
val isVpnMode = SettingsManager.isVpnMode()
val intent = if (isVpnMode) {
Log.i(AppConfig.TAG, "StartCore-Manager: Starting VPN service")
Intent(context.applicationContext, V2RayVpnService::class.java)
} else {
Log.i(AppConfig.TAG, "StartCore-Manager: Starting Proxy service")
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
}
ContextCompat.startForegroundService(context, intent)
try {
ContextCompat.startForegroundService(context, intent)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to start service", e)
}
}
/**
@@ -123,15 +150,34 @@ object V2RayServiceManager {
*/
fun startCoreLoop(vpnInterface: ParcelFileDescriptor?): Boolean {
if (coreController.isRunning) {
Log.w(AppConfig.TAG, "StartCore-Manager: Core already running")
return false
}
val service = getService() ?: return false
val guid = MmkvManager.getSelectServer() ?: return false
val config = MmkvManager.decodeServerConfig(guid) ?: return false
val result = V2rayConfigManager.getV2rayConfig(service, guid)
if (!result.status)
val service = getService()
if (service == null) {
Log.e(AppConfig.TAG, "StartCore-Manager: Service is null")
return false
}
val guid = MmkvManager.getSelectServer()
if (guid == null) {
Log.e(AppConfig.TAG, "StartCore-Manager: No server selected")
return false
}
val config = MmkvManager.decodeServerConfig(guid)
if (config == null) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
return false
}
Log.i(AppConfig.TAG, "StartCore-Manager: Starting core loop for ${config.remarks}")
val result = V2rayConfigManager.getV2rayConfig(service, guid)
if (!result.status) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to get V2Ray config")
return false
}
try {
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
@@ -140,7 +186,7 @@ object V2RayServiceManager {
mFilter.addAction(Intent.ACTION_USER_PRESENT)
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e)
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to register receiver", e)
return false
}
@@ -154,11 +200,12 @@ object V2RayServiceManager {
NotificationManager.showNotification(currentConfig)
coreController.startLoop(result.content, tunFd)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to start Core loop", e)
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to start core loop", e)
return false
}
if (coreController.isRunning == false) {
Log.e(AppConfig.TAG, "StartCore-Manager: Core failed to start")
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
NotificationManager.cancelNotification()
return false
@@ -166,11 +213,10 @@ object V2RayServiceManager {
try {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
//NotificationManager.showNotification(currentConfig)
NotificationManager.startSpeedNotification(currentConfig)
Log.i(AppConfig.TAG, "StartCore-Manager: Core started successfully")
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to startup service", e)
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to complete startup", e)
return false
}
return true
@@ -189,7 +235,7 @@ object V2RayServiceManager {
try {
coreController.stopLoop()
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e)
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to stop V2Ray loop", e)
}
}
}
@@ -200,7 +246,7 @@ object V2RayServiceManager {
try {
service.unregisterReceiver(mMsgReceive)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e)
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to unregister receiver", e)
}
return true
@@ -234,14 +280,14 @@ object V2RayServiceManager {
try {
time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e)
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
errorStr = e.message?.substringAfter("\":") ?: "empty message"
}
if (time == -1L) {
try {
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e)
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
errorStr = e.message?.substringAfter("\":") ?: "empty message"
}
}
@@ -293,7 +339,7 @@ object V2RayServiceManager {
serviceControl.stopService()
0
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to stop service in callback", e)
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to stop service", e)
-1
}
}
@@ -340,12 +386,12 @@ object V2RayServiceManager {
}
AppConfig.MSG_STATE_STOP -> {
Log.i(AppConfig.TAG, "Stop Service")
Log.i(AppConfig.TAG, "StartCore-Manager: Stop service")
serviceControl.stopService()
}
AppConfig.MSG_STATE_RESTART -> {
Log.i(AppConfig.TAG, "Restart Service")
Log.i(AppConfig.TAG, "StartCore-Manager: Restart service")
serviceControl.stopService()
Thread.sleep(500L)
startVService(serviceControl.getService())
@@ -358,12 +404,12 @@ object V2RayServiceManager {
when (intent?.action) {
Intent.ACTION_SCREEN_OFF -> {
Log.i(AppConfig.TAG, "SCREEN_OFF, stop querying stats")
Log.i(AppConfig.TAG, "StartCore-Manager: Screen off")
NotificationManager.stopSpeedNotification(currentConfig)
}
Intent.ACTION_SCREEN_ON -> {
Log.i(AppConfig.TAG, "SCREEN_ON, start querying stats")
Log.i(AppConfig.TAG, "StartCore-Manager: Screen on")
NotificationManager.startSpeedNotification(currentConfig)
}
}
@@ -467,6 +467,19 @@ object V2rayConfigManager {
val rule = JsonUtil.fromJson(JsonUtil.toJson(item), RulesBean::class.java) ?: return
// Replace specific geoip rules with ext versions
rule.ip?.let { ipList ->
val updatedIpList = ArrayList<String>()
ipList.forEach { ip ->
when (ip) {
AppConfig.GEOIP_CN -> updatedIpList.add("ext:${AppConfig.GEOIP_ONLY_CN_PRIVATE_DAT}:cn")
AppConfig.GEOIP_PRIVATE -> updatedIpList.add("ext:${AppConfig.GEOIP_ONLY_CN_PRIVATE_DAT}:private")
else -> updatedIpList.add(ip)
}
}
rule.ip = updatedIpList
}
v2rayConfig.routing.rules.add(rule)
} catch (e: Exception) {
@@ -3,6 +3,8 @@ package com.v2ray.ang.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2RayServiceManager
@@ -16,8 +18,24 @@ class BootReceiver : BroadcastReceiver() {
* @param intent The Intent being received.
*/
override fun onReceive(context: Context?, intent: Intent?) {
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return
if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
Log.i(AppConfig.TAG, "BootReceiver received: ${intent?.action}")
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) {
Log.w(AppConfig.TAG, "BootReceiver: Invalid context or action")
return
}
if (!MmkvManager.decodeStartOnBoot()) {
Log.i(AppConfig.TAG, "BootReceiver: Auto-start on boot is disabled")
return
}
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
Log.w(AppConfig.TAG, "BootReceiver: No server selected")
return
}
Log.i(AppConfig.TAG, "BootReceiver: Starting V2Ray service")
V2RayServiceManager.startVService(context)
}
}
@@ -4,6 +4,8 @@ import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.contracts.ServiceControl
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.V2RayServiceManager
@@ -16,6 +18,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
*/
override fun onCreate() {
super.onCreate()
Log.i(AppConfig.TAG, "StartCore-Proxy: Service created")
V2RayServiceManager.serviceControl = SoftReference(this)
}
@@ -27,6 +30,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
* @return The start mode.
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(AppConfig.TAG, "StartCore-Proxy: Service command received")
V2RayServiceManager.startCoreLoop(null)
return START_STICKY
}
@@ -6,6 +6,8 @@ import android.os.IBinder
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
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.V2RayNativeManager
import com.v2ray.ang.util.MessageUtil
@@ -52,13 +54,15 @@ class V2RayTestService : Service() {
* @return The start mode.
*/
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 -> {
val subscriptionId = intent.getStringExtra("content").orEmpty()
val guidsList = if (subscriptionId.isEmpty()) {
MmkvManager.decodeAllServerList()
val guidsList = if (message.serverGuids.isNotEmpty()) {
message.serverGuids
} else if (message.subscriptionId.isNotEmpty()) {
MmkvManager.decodeServerList(message.subscriptionId)
} else {
MmkvManager.decodeServerList(subscriptionId)
MmkvManager.decodeAllServerList()
}
if (guidsList.isNotEmpty()) {
@@ -1,5 +1,6 @@
package com.v2ray.ang.service
import android.annotation.SuppressLint
import android.app.Service
import android.content.Context
import android.content.Intent
@@ -28,6 +29,7 @@ import com.v2ray.ang.util.MyContextWrapper
import com.v2ray.ang.util.Utils
import java.lang.ref.SoftReference
@SuppressLint("VpnServicePolicy")
class V2RayVpnService : VpnService(), ServiceControl {
private lateinit var mInterface: ParcelFileDescriptor
private var isRunning = false
@@ -72,12 +74,14 @@ class V2RayVpnService : VpnService(), ServiceControl {
override fun onCreate() {
super.onCreate()
Log.i(AppConfig.TAG, "StartCore-VPN: Service created")
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
StrictMode.setThreadPolicy(policy)
V2RayServiceManager.serviceControl = SoftReference(this)
}
override fun onRevoke() {
Log.w(AppConfig.TAG, "StartCore-VPN: Permission revoked")
stopAllService()
}
@@ -88,10 +92,12 @@ class V2RayVpnService : VpnService(), ServiceControl {
override fun onDestroy() {
super.onDestroy()
Log.i(AppConfig.TAG, "StartCore-VPN: Service destroyed")
NotificationManager.cancelNotification()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(AppConfig.TAG, "StartCore-VPN: Service command received")
setupVpnService()
startService()
return START_STICKY
@@ -103,12 +109,12 @@ class V2RayVpnService : VpnService(), ServiceControl {
}
override fun startService() {
if (mInterface == null) {
Log.e(AppConfig.TAG, "Failed to create VPN interface")
if (!::mInterface.isInitialized) {
Log.e(AppConfig.TAG, "StartCore-VPN: Interface not initialized")
return
}
if (!V2RayServiceManager.startCoreLoop(mInterface)) {
Log.e(AppConfig.TAG, "Failed to start V2Ray core loop")
Log.e(AppConfig.TAG, "StartCore-VPN: Failed to start core loop")
stopAllService()
return
}
@@ -136,13 +142,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
private fun setupVpnService() {
val prepare = prepare(this)
if (prepare != null) {
Log.e(AppConfig.TAG, "VPN preparation failed")
Log.e(AppConfig.TAG, "StartCore-VPN: Permission not granted")
stopSelf()
return
}
if (configureVpnService() != true) {
Log.e(AppConfig.TAG, "VPN configuration failed")
Log.e(AppConfig.TAG, "StartCore-VPN: Configuration failed")
stopSelf()
return
}
@@ -165,9 +171,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
// Close the old interface since the parameters have been changed
try {
mInterface.close()
} catch (ignored: Exception) {
// ignored
if (::mInterface.isInitialized) {
mInterface.close()
}
} catch (e: Exception) {
Log.w(AppConfig.TAG, "Failed to close old interface", e)
}
// Configure platform-specific features
@@ -244,7 +252,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
try {
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to request default network", e)
Log.e(AppConfig.TAG, "StartCore-VPN: Failed to request network", e)
}
}
@@ -297,7 +305,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
builder.addAllowedApplication(it)
}
} catch (e: PackageManager.NameNotFoundException) {
Log.e(AppConfig.TAG, "Failed to configure app in VPN: ${e.localizedMessage}", e)
Log.e(AppConfig.TAG, "StartCore-VPN: Failed to configure app", e)
}
}
}
@@ -330,8 +338,8 @@ class V2RayVpnService : VpnService(), ServiceControl {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try {
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
} catch (ignored: Exception) {
// ignored
} catch (e: Exception) {
Log.w(AppConfig.TAG, "StartCore-VPN: Failed to unregister callback", e)
}
}
@@ -349,9 +357,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
stopSelf()
try {
mInterface.close()
if (::mInterface.isInitialized) {
mInterface.close()
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to close VPN interface", e)
Log.e(AppConfig.TAG, "StartCore-VPN: Failed to close interface", e)
}
}
}
@@ -437,14 +437,23 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
showLoading()
lifecycleScope.launch(Dispatchers.IO) {
val count = mainViewModel.updateConfigViaSubAll()
val result = mainViewModel.updateConfigViaSubAll()
delay(500L)
launch(Dispatchers.Main) {
if (count > 0) {
toast(getString(R.string.title_update_config_count, count))
mainViewModel.reloadServerList()
if (result.successCount + result.failureCount + result.skipCount == 0) {
toast(R.string.title_update_subscription_no_subscription)
} else if (result.successCount > 0 && result.failureCount + result.skipCount == 0) {
toast(getString(R.string.title_update_config_count, result.configCount))
} 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()
}
@@ -14,6 +14,7 @@ import com.v2ray.ang.extension.toast
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.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -118,7 +119,7 @@ class SubEditActivity : BaseActivity() {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch(Dispatchers.IO) {
MmkvManager.removeSubscription(editSubId)
SettingsManager.removeSubscriptionWithDefault(editSubId)
launch(Dispatchers.Main) {
finish()
}
@@ -130,7 +131,7 @@ class SubEditActivity : BaseActivity() {
.show()
} else {
lifecycleScope.launch(Dispatchers.IO) {
MmkvManager.removeSubscription(editSubId)
SettingsManager.removeSubscriptionWithDefault(editSubId)
launch(Dispatchers.Main) {
finish()
}
@@ -145,10 +146,6 @@ class SubEditActivity : BaseActivity() {
del_config = menu.findItem(R.id.del_config)
save_config = menu.findItem(R.id.save_config)
if (editSubId.isEmpty() || editSubId == AppConfig.DEFAULT_SUBSCRIPTION_ID) {
del_config?.isVisible = false
}
return super.onCreateOptionsMenu(menu)
}
@@ -18,8 +18,6 @@ import com.v2ray.ang.contracts.BaseAdapterListener
import com.v2ray.ang.databinding.ActivitySubSettingBinding
import com.v2ray.ang.databinding.ItemQrcodeBinding
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.MmkvManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
@@ -77,14 +75,20 @@ class SubSettingActivity : BaseActivity() {
showLoading()
lifecycleScope.launch(Dispatchers.IO) {
val count = AngConfigManager.updateConfigViaSubAll()
val result = AngConfigManager.updateConfigViaSubAll()
delay(500L)
launch(Dispatchers.Main) {
if (count > 0) {
toastSuccess(R.string.toast_success)
refreshData()
if (result.successCount + result.failureCount + result.skipCount == 0) {
toast(R.string.title_update_subscription_no_subscription)
} else if (result.successCount > 0 && result.failureCount + result.skipCount == 0) {
toast(getString(R.string.title_update_config_count, result.configCount))
} else {
toastError(R.string.toast_failure)
toast(
getString(
R.string.title_update_subscription_result,
result.configCount, result.successCount, result.failureCount, result.skipCount
)
)
}
hideLoading()
}
@@ -39,7 +39,6 @@ class SubSettingRecyclerAdapter(
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
adapterListener?.onRemove(subId, position)
}
holder.itemSubSettingBinding.layoutRemove.isVisible = subId != AppConfig.DEFAULT_SUBSCRIPTION_ID
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
if (!it.isPressed) return@setOnCheckedChangeListener
@@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.TestServiceMessage
import com.v2ray.ang.service.V2RayTestService
import java.io.Serializable
@@ -37,15 +38,13 @@ object MessageUtil {
* Sends a message to the test service.
*
* @param ctx The context.
* @param what The message identifier.
* @param content The message content.
* @param message The test service message containing key, subscriptionId, and serverGuids.
*/
fun sendMsg2TestService(ctx: Context, what: Int, content: Serializable) {
fun sendMsg2TestService(ctx: Context, message: TestServiceMessage) {
try {
val intent = Intent()
intent.component = ComponentName(ctx, V2RayTestService::class.java)
intent.putExtra("key", what)
intent.putExtra("content", content)
intent.putExtra("content", message)
ctx.startService(intent)
} catch (e: Exception) {
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.ServersCache
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.toastError
import com.v2ray.ang.extension.toastSuccess
@@ -37,8 +39,6 @@ import java.util.Collections
class MainViewModel(application: Application) : AndroidViewModel(application) {
private var serverList = mutableListOf<String>() // MmkvManager.decodeServerList()
var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
//var keywordFilter: String = MmkvManager.MmkvManager.decodeSettingsString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
var keywordFilter = ""
val serversCache = mutableListOf<ServersCache>()
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.
* @param fromPosition The initial position of the server.
@@ -149,26 +117,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
@Synchronized
fun updateCache() {
serversCache.clear()
val kw = keywordFilter.trim().lowercase()
for (guid in serverList) {
val profile = MmkvManager.decodeServerConfig(guid) ?: continue
// var profile = MmkvManager.decodeProfileConfig(guid)
// if (profile == null) {
// val config = MmkvManager.decodeServerConfig(guid) ?: 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 (kw.isEmpty()) {
serversCache.add(ServersCache(guid, profile))
continue
}
// if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) {
// continue
// }
val remarks = profile.remarks.lowercase()
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))
}
}
@@ -176,13 +137,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
/**
* 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()) {
return AngConfigManager.updateConfigViaSubAll()
} else {
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return 0
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return SubscriptionUpdateResult()
return AngConfigManager.updateConfigViaSub(SubscriptionCache(subscriptionId, subItem))
}
}
@@ -236,7 +197,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
* Tests the real ping for all servers.
*/
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())
updateListAction.value = -1
@@ -244,7 +208,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
if (serversCache.isEmpty()) {
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()
)
)
}
}
@@ -281,9 +252,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
val groups = mutableListOf<GroupMapItem>()
if (subscriptions.size > 1
&& MmkvManager.decodeSettingsBool(AppConfig.PREF_GROUP_ALL_DISPLAY)
) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_GROUP_ALL_DISPLAY)) {
groups.add(
GroupMapItem(
id = "",
@@ -430,7 +399,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return
}
keywordFilter = keyword
MmkvManager.encodeSettings(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
reloadServerList()
}
@@ -21,7 +21,7 @@ class SubscriptionsViewModel : ViewModel() {
fun remove(subId: String): Boolean {
val changed = subscriptions.removeAll { it.guid == subId }
if (changed) {
MmkvManager.removeSubscription(subId)
SettingsManager.removeSubscriptionWithDefault(subId)
SettingsChangeManager.makeSetupGroupTab()
}
return changed
@@ -15,7 +15,7 @@ import java.net.HttpURLConnection
class UserAssetViewModel : ViewModel() {
private val assets = mutableListOf<AssetUrlCache>()
private val builtInGeoFiles = listOf("geosite.dat", "geoip.dat")
private val builtInGeoFiles = listOf(AppConfig.GEOSITE_DAT, AppConfig.GEOIP_DAT, AppConfig.GEOIP_ONLY_CN_PRIVATE_DAT)
val itemCount: Int
get() = assets.size
@@ -47,7 +47,18 @@ class UserAssetViewModel : ViewModel() {
)
)
}
return builtInItems + savedAssets
// Force update URL for geoip-only-cn-private.dat
return (builtInItems + savedAssets).map { cache ->
if (cache.assetUrl.remarks == AppConfig.GEOIP_ONLY_CN_PRIVATE_DAT) {
cache.copy(
assetUrl = cache.assetUrl.copy(
url = AppConfig.GEOIP_ONLY_CN_PRIVATE_URL
)
)
} else {
cache
}
}
}
fun downloadGeoFiles(extDir: File, httpPort: Int): GeoDownloadResult {
@@ -297,6 +297,8 @@
<string name="title_import_config_count">Import %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_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_setting_confirm">تأكيد</string>
@@ -296,6 +296,8 @@
<string name="title_import_config_count">Import %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_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_setting_confirm">নিশ্চিত করুন</string>
@@ -234,8 +234,8 @@
<string name="title_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="summary_pref_group_all_display">Add an extra "All Tabs" page</string>
<string name="title_pref_group_all_display">نشووݩ داڌن پوی بونکۊ یل ن فعال کۊنین</string>
<string name="summary_pref_group_all_display">ی بلگه «پوی بلگه یل» ازاف کۊنین</string>
<!-- AboutActivity -->
<string name="title_pref_feedback">فشناڌن منشڌ</string>
<string name="summary_pref_feedback">فشناڌن منشڌ یا گوزارش موشکلا من Github</string>
@@ -297,6 +297,8 @@
<string name="title_import_config_count">و من ٱووردن %d کانفیگ</string>
<string name="title_export_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_setting_confirm">قوۊل</string>
@@ -417,7 +419,7 @@
<string-array name="policy_group_type">
<item>کم ترین پینگ</item>
<item>کم ترین بار(لود)</item>
<item>تصادفی</item>
<item>تساڌۊفی</item>
<item>گردشی</item>
</string-array>
@@ -294,6 +294,8 @@
<string name="title_import_config_count">وارد کردن %d کانفیگ</string>
<string name="title_export_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_setting_confirm">تایید</string>
@@ -233,8 +233,8 @@
<string name="title_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="summary_pref_group_all_display">Add an extra "All Tabs" page</string>
<string name="title_pref_group_all_display">Общая вкладка групп</string>
<string name="summary_pref_group_all_display">Показывать дополнительную вкладку со всеми профилями групп</string>
<!-- AboutActivity -->
<string name="title_pref_feedback">Обратная связь</string>
<string name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</string>
@@ -295,6 +295,8 @@
<string name="title_import_config_count">Импортировано профилей: %d</string>
<string name="title_export_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_setting_confirm">Подтвердить</string>
@@ -358,7 +360,7 @@
<string name="title_configuration_restore">Восстановление конфигурации</string>
<string name="title_configuration_share">Поделиться конфигурацией</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_user">Пользователь</string>
<string name="title_webdav_pass">Пароль</string>
@@ -297,6 +297,8 @@
<string name="title_import_config_count">Import %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_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_setting_confirm">Xác nhận</string>
@@ -295,6 +295,8 @@
<string name="title_import_config_count">导入 %d 个配置</string>
<string name="title_export_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_setting_confirm">确定</string>
@@ -295,6 +295,8 @@
<string name="title_import_config_count">匯入 %d 個配置</string>
<string name="title_export_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_setting_confirm">確定</string>
@@ -301,6 +301,8 @@
<string name="title_import_config_count">Import %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_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_setting_confirm">Confirm</string>
+1 -1
View File
@@ -1,5 +1,5 @@
[versions]
agp = "9.0.1"
agp = "9.1.0"
desugarJdkLibs = "2.1.5"
gradleLicensePlugin = "0.9.8"
kotlin = "2.3.10"