18 Commits

Author SHA1 Message Date
zarazaex69 f58a4f8f6f style: fix indentation and mark non-translatable strings 2026-04-11 00:37:14 +03:00
zarazaex69 fdce3ea2c1 fix(service): prevent concurrent operations with synchronization locks 2026-04-11 00:31:28 +03:00
zarazaex69 f0d620676d chore(gradle): downgrade Kotlin version to 2.1.0 2026-04-10 18:41:54 +03:00
zarazaex69 b92a6cdfac fix(speedtest): improve connection test with IP validation 2026-04-10 18:36:36 +03:00
zarazaex69 45da2479dd fix(v2ray): remove unused VPN DNS server retrieval 2026-04-10 18:14:58 +03:00
zarazaex69 1c936b2b31 fix(v2ray): simplify DNS server configuration logic 2026-04-10 17:50:56 +03:00
zarazaex69 dbe109eedb chore(mmkv): update database files and checksums 2026-04-10 17:41:59 +03:00
zarazaex69 7705aded77 fix(settings): validate VPN DNS servers with IP address check 2026-04-10 17:28:56 +03:00
zarazaex69 4a2d62b671 feat(dns): improve VPN DNS configuration and filtering 2026-04-10 16:33:32 +03:00
zarazaex69 4acca4e554 chore: remove unused list file 2026-04-10 16:19:23 +03:00
zarazaex69 b875613fb3 chore(mmkv): update MMKV database files and checksums 2026-04-10 12:55:40 +03:00
zarazaex69 d9d21061fa chore(mmkv): update SETTING database files 2026-04-10 12:52:23 +03:00
zarazaex69 fc7804fc1e chore(mmkv): update SETTING database files 2026-04-10 12:46:03 +03:00
zarazaex69 4e9de615a4 chore(mmkv): update asset database files and remove unused ASSET store 2026-04-10 12:43:38 +03:00
zarazaex69 7617ce898c chore: remove KEY subscription setup script 2026-04-10 12:41:09 +03:00
zarazaex69 8d284fd68a feat(subscription): add KEY subscription s 2026-04-10 11:49:49 +03:00
zarazaex69 0f28310801 refactor(config): deduplicate profiles during batch save operation 2026-04-10 11:27:30 +03:00
zarazaex69 a46123aeab chore(build): update APK binary with latest changes 2026-04-10 11:24:55 +03:00
25 changed files with 164 additions and 74 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -125,7 +125,7 @@ object AppConfig {
const val DNS_PROXY = "https://1.1.1.1/dns-query"
const val DNS_DIRECT = "223.5.5.5"
const val DNS_VPN = "https://1.1.1.1/dns-query"
const val DNS_VPN = "1.1.1.1"
const val GEOSITE_PRIVATE = "geosite:private"
const val GEOSITE_CN = "geosite:cn"
const val GEOIP_PRIVATE = "geoip:private"
@@ -273,26 +273,35 @@ object AngConfigManager {
private fun batchSaveConfigs(configs: List<ProfileItem>, subid: String): Map<String, ProfileItem> {
val keyToProfile = mutableMapOf<String, ProfileItem>()
// Read serverList once
val serverList = MmkvManager.decodeServerList(subid)
var needSetSelected = MmkvManager.getSelectServer().isNullOrBlank()
configs.forEach { config ->
val key = Utils.getUuid()
// Save profile directly without updating serverList
MmkvManager.encodeProfileDirect(key, JsonUtil.toJson(config))
val existingProfiles = serverList.mapNotNull { guid ->
MmkvManager.decodeServerConfig(guid)?.let { guid to it }
}.toMap()
if (!serverList.contains(key)) {
serverList.add(0, key)
if (needSetSelected) {
MmkvManager.setSelectServer(key)
needSetSelected = false
configs.forEach { config ->
val existingKey = existingProfiles.entries.firstOrNull { (_, existing) ->
existing == config
}?.key
if (existingKey != null) {
keyToProfile[existingKey] = config
} else {
val key = Utils.getUuid()
MmkvManager.encodeProfileDirect(key, JsonUtil.toJson(config))
if (!serverList.contains(key)) {
serverList.add(0, key)
if (needSetSelected) {
MmkvManager.setSelectServer(key)
needSetSelected = false
}
}
keyToProfile[key] = config
}
keyToProfile[key] = config
}
// Write serverList once
MmkvManager.encodeServerList(serverList, subid)
return keyToProfile
}
@@ -346,7 +346,7 @@ object SettingsManager {
*/
fun getVpnDnsServers(): List<String> {
val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
return vpnDns.split(",").filter { Utils.isPureIpAddress(it) }
return vpnDns.split(",").filter { it.isNotBlank() && Utils.isPureIpAddress(it) }
}
/**
@@ -94,18 +94,23 @@ object SpeedtestManager {
var result: String
var elapsed = -1L
val conn = HttpUtil.createProxyConnection(SettingsManager.getDelayTestUrl(), port, 15000, 15000) ?: return Pair(elapsed, "")
val testUrl = "https://icanhazip.com"
val conn = HttpUtil.createProxyConnection(testUrl, port, 15000, 15000) ?: return Pair(elapsed, "")
try {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
if (code != 200) {
throw IOException(context.getString(R.string.connection_test_error_status_code, code))
}
val responseBody = conn.inputStream.bufferedReader().readText().trim()
elapsed = SystemClock.elapsedRealtime() - start
result = when (code) {
204 -> context.getString(R.string.connection_test_available, elapsed)
200 if conn.contentLengthLong == 0L -> context.getString(R.string.connection_test_available, elapsed)
else -> throw IOException(
context.getString(R.string.connection_test_error_status_code, code)
)
if (xyz.zarazaex.olc.util.Utils.isPureIpAddress(responseBody)) {
result = context.getString(R.string.connection_test_available, elapsed)
} else {
throw IOException("Invalid IP response: $responseBody")
}
} catch (e: IOException) {
Log.e(AppConfig.TAG, "Connection test IOException", e)
@@ -30,6 +30,8 @@ object V2RayServiceManager {
private val coreController: CoreController = V2RayNativeManager.newCoreController(CoreCallback())
private val mMsgReceive = ReceiveMessageHandler()
private var currentConfig: ProfileItem? = null
private val operationLock = Any()
@Volatile private var isOperationInProgress = false
var serviceControl: SoftReference<ServiceControl>? = null
set(value) {
@@ -57,13 +59,27 @@ 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)
synchronized(operationLock) {
if (isOperationInProgress) {
Log.w(AppConfig.TAG, "StartCore-Manager: Operation already in progress")
return
}
isOperationInProgress = true
}
startContextService(context)
try {
Log.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}")
if (guid != null) {
MmkvManager.setSelectServer(guid)
}
startContextService(context)
} finally {
synchronized(operationLock) {
isOperationInProgress = false
}
}
}
/**
@@ -71,8 +87,21 @@ object V2RayServiceManager {
* @param context The context from which the service is stopped.
*/
fun stopVService(context: Context) {
//context.toast(R.string.toast_services_stop)
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
synchronized(operationLock) {
if (isOperationInProgress) {
Log.w(AppConfig.TAG, "StartCore-Manager: Operation already in progress")
return
}
isOperationInProgress = true
}
try {
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
} finally {
synchronized(operationLock) {
isOperationInProgress = false
}
}
}
/**
@@ -385,11 +414,17 @@ object V2RayServiceManager {
AppConfig.MSG_STATE_STOP -> {
Log.i(AppConfig.TAG, "StartCore-Manager: Stop service")
synchronized(operationLock) {
isOperationInProgress = false
}
serviceControl.stopService()
}
AppConfig.MSG_STATE_RESTART -> {
Log.i(AppConfig.TAG, "StartCore-Manager: Restart service")
synchronized(operationLock) {
isOperationInProgress = false
}
serviceControl.stopService()
Thread.sleep(500L)
startVService(serviceControl.getService())
@@ -1311,7 +1311,7 @@ object V2rayConfigManager {
if (start != null && end != null) {
val minStart = maxOf(5, start)
val minEnd = maxOf(minStart, end)
"$minStart-$minEnd"
"$minStart-$minEnd"
} else {
"30"
}
@@ -228,13 +228,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
}
}
// Configure DNS servers
//if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
// builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
//} else {
SettingsManager.getVpnDnsServers().forEach {
if (Utils.isPureIpAddress(it)) {
builder.addDnsServer(it)
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
builder.addDnsServer(vpnConfig.ipv4Router)
} else {
SettingsManager.getVpnDnsServers().forEach {
if (Utils.isPureIpAddress(it)) {
builder.addDnsServer(it)
}
}
}
@@ -42,9 +42,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelectedListener {
private val binding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
private val binding by lazy {ActivityMainBinding.inflate(layoutInflater)}
private var isLiteTesting = false
private var easterEggClickCount = 0
private var isEasterEggActive = false
@@ -52,6 +50,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
val mainViewModel: MainViewModel by viewModels()
private lateinit var groupPagerAdapter: GroupPagerAdapter
private var tabMediator: TabLayoutMediator? = null
@Volatile private var isFabOperationInProgress = false
private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
@@ -201,12 +200,24 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
}
private fun handleFabAction() {
if (isFabOperationInProgress) {
return
}
isFabOperationInProgress = true
applyRunningState(isLoading = true, isRunning = false)
if (mainViewModel.isRunning.value == true) {
V2RayServiceManager.stopVService(this)
} else {
startV2RayWithPermission()
lifecycleScope.launch {
try {
if (mainViewModel.isRunning.value == true) {
V2RayServiceManager.stopVService(this@MainActivity)
} else {
startV2RayWithPermission()
}
} finally {
delay(1000)
isFabOperationInProgress = false
}
}
}
@@ -220,29 +231,42 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
}
private fun handleLiteAction() {
if (mainViewModel.isRunning.value == true) {
V2RayServiceManager.stopVService(this)
if (isFabOperationInProgress) {
return
}
showStatus("Обновление профилей...")
showLoading()
isLiteTesting = true
lifecycleScope.launch(Dispatchers.IO) {
val result = mainViewModel.updateConfigViaSubAll()
delay(500L)
launch(Dispatchers.Main) {
if (result.configCount > 0) {
mainViewModel.reloadServerList()
showStatus("Обновлено ${result.configCount} профилей. Запуск теста...")
} else {
showStatus("Запуск теста...")
isFabOperationInProgress = true
lifecycleScope.launch {
try {
if (mainViewModel.isRunning.value == true) {
V2RayServiceManager.stopVService(this@MainActivity)
delay(1000)
}
hideLoading()
delay(500L)
showStatus("Выполняется замер задержки. Ожидаем завершения...")
mainViewModel.testAllRealPing()
showStatus("Обновление профилей...")
showLoading()
isLiteTesting = true
launch(Dispatchers.IO) {
val result = mainViewModel.updateConfigViaSubAll()
delay(500L)
launch(Dispatchers.Main) {
if (result.configCount > 0) {
mainViewModel.reloadServerList()
showStatus("Обновлено ${result.configCount} профилей. Запуск теста...")
} else {
showStatus("Запуск теста...")
}
hideLoading()
delay(500L)
showStatus("Выполняется замер задержки. Ожидаем завершения...")
mainViewModel.testAllRealPing()
}
}
} finally {
delay(1000)
isFabOperationInProgress = false
}
}
}
@@ -269,12 +293,22 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
}
fun restartV2Ray() {
if (mainViewModel.isRunning.value == true) {
V2RayServiceManager.stopVService(this)
if (isFabOperationInProgress) {
return
}
isFabOperationInProgress = true
lifecycleScope.launch {
delay(500)
startV2Ray()
try {
if (mainViewModel.isRunning.value == true) {
V2RayServiceManager.stopVService(this@MainActivity)
}
delay(1000)
startV2Ray()
} finally {
delay(500)
isFabOperationInProgress = false
}
}
}
+3 -3
View File
@@ -7,8 +7,8 @@
<string name="navigation_drawer_open">Open navigation drawer</string>
<string name="navigation_drawer_close">Close navigation drawer</string>
<string name="migration_success">Data migration success!</string>
<string name="drawer_forked_text">forked from <a href="https://github.com/2dust/v2rayng">V2RayNG</a></string>
<string name="drawer_developed_text">developed by developers from <a href="https://t.me/openlibrecommunity">Olc</a></string>
<string name="drawer_forked_text" translatable="false">forked from <a href="https://github.com/2dust/v2rayng">V2RayNG</a></string>
<string name="drawer_developed_text" translatable="false">developed by developers from <a href="https://t.me/openlibrecommunity">Olc</a></string>
<string name="action_stop_service">Stop service</string>
<string name="migration_fail">Data migration failed!</string>
<string name="pull_down_to_refresh">Please pull down to refresh!</string>
@@ -249,7 +249,7 @@
<string name="summary_pref_tg_group">Join Telegram Group</string>
<string name="toast_tg_app_not_found">Telegram app not found</string>
<string name="title_privacy_policy">Privacy policy</string>
<string name="title_qr_code">QR code</string>
<string name="title_qr_code" translatable="false">QR code</string>
<string name="title_about">About</string>
<string name="title_source_code">Source code</string>
<string name="title_oss_license">Open Source licenses</string>
+1 -1
View File
@@ -2,7 +2,7 @@
agp = "9.1.0"
desugarJdkLibs = "2.1.5"
gradleLicensePlugin = "0.9.8"
kotlin = "2.3.10"
kotlin = "2.1.0"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
BIN
View File
Binary file not shown.
+8
View File
@@ -136,9 +136,17 @@ fun main() {
)
println("Добавлена подписка БЕЛЫЕ W: $guid2")
val guid3 = manager.addSubscription(
remarks = "KEY",
url = "https://key.zarazaex.xyz/sub",
autoUpdate = true
)
println("Добавлена подписка KEY: $guid3")
println("\nОбновление подписок...")
manager.updateSubscription(guid1, "https://raw.githubusercontent.com/zieng2/wl/refs/heads/main/vless_universal.txt")
manager.updateSubscription(guid2, "https://raw.githubusercontent.com/whoahaow/rjsxrd/refs/heads/main/githubmirror/bypass/bypass-all.txt")
manager.updateSubscription(guid3, "https://key.zarazaex.xyz/sub")
println("\nПодписки успешно добавлены и обновлены в $mmkvPath")
}
-1
View File
@@ -1 +0,0 @@
ТЫ