mirror of
https://github.com/openlibrecommunity/olcng.git
synced 2026-07-03 14:05:17 +02:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb4e3aab54 | |||
| 732d7248af | |||
| 0c32ddc642 | |||
| 153c545400 | |||
| 9af6472f8b | |||
| 346be42e37 | |||
| dd9e076ab1 | |||
| 903b0d4414 | |||
| f58a4f8f6f | |||
| fdce3ea2c1 | |||
| f0d620676d | |||
| b92a6cdfac | |||
| 45da2479dd | |||
| 1c936b2b31 | |||
| dbe109eedb | |||
| 7705aded77 |
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.
@@ -0,0 +1,15 @@
|
|||||||
|
package xyz.zarazaex.olc.dto
|
||||||
|
|
||||||
|
enum class SubscriptionUpdateStatus {
|
||||||
|
IDLE,
|
||||||
|
LOADING,
|
||||||
|
SUCCESS,
|
||||||
|
FAILED,
|
||||||
|
SKIPPED
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SubscriptionStatus(
|
||||||
|
val guid: String,
|
||||||
|
val status: SubscriptionUpdateStatus = SubscriptionUpdateStatus.IDLE,
|
||||||
|
val configCount: Int = 0
|
||||||
|
)
|
||||||
@@ -29,6 +29,14 @@ import java.net.URI
|
|||||||
|
|
||||||
object AngConfigManager {
|
object AngConfigManager {
|
||||||
|
|
||||||
|
private val subscriptionLocks = mutableMapOf<String, Any>()
|
||||||
|
|
||||||
|
private fun getSubscriptionLock(subid: String): Any {
|
||||||
|
return synchronized(subscriptionLocks) {
|
||||||
|
subscriptionLocks.getOrPut(subid) { Any() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shares the configuration to the clipboard.
|
* Shares the configuration to the clipboard.
|
||||||
@@ -217,49 +225,55 @@ object AngConfigManager {
|
|||||||
* @return The number of configurations parsed.
|
* @return The number of configurations parsed.
|
||||||
*/
|
*/
|
||||||
private fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
|
private fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
|
||||||
try {
|
return synchronized(getSubscriptionLock(subid)) {
|
||||||
if (servers == null) {
|
try {
|
||||||
return 0
|
if (servers == null) {
|
||||||
}
|
return@synchronized 0
|
||||||
// Find the currently selected server that matches the subscription ID
|
}
|
||||||
val removedSelected = if (subid.isNotBlank() && !append) {
|
val removedSelected = if (subid.isNotBlank() && !append) {
|
||||||
MmkvManager.getSelectServer()
|
MmkvManager.getSelectServer()
|
||||||
.takeIf { it?.isNotBlank() == true }
|
.takeIf { it?.isNotBlank() == true }
|
||||||
?.let { MmkvManager.decodeServerConfig(it) }
|
?.let { MmkvManager.decodeServerConfig(it) }
|
||||||
?.takeIf { it.subscriptionId == subid }
|
?.takeIf { it.subscriptionId == subid }
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
val subItem = MmkvManager.decodeSubscription(subid)
|
val subItem = MmkvManager.decodeSubscription(subid)
|
||||||
|
|
||||||
// Parse all configs first (no I/O during parsing)
|
val oldPingData = if (!append) {
|
||||||
val configs = mutableListOf<ProfileItem>()
|
saveOldPingData(subid)
|
||||||
servers.lines()
|
} else {
|
||||||
.distinct()
|
emptyMap()
|
||||||
.reversed()
|
}
|
||||||
.forEach {
|
|
||||||
val config = parseConfig(it, subid, subItem)
|
val configs = mutableListOf<ProfileItem>()
|
||||||
if (config != null) {
|
servers.lines()
|
||||||
configs.add(config)
|
.distinct()
|
||||||
|
.reversed()
|
||||||
|
.forEach {
|
||||||
|
val config = parseConfig(it, subid, subItem)
|
||||||
|
if (config != null) {
|
||||||
|
configs.add(config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (configs.isNotEmpty()) {
|
||||||
|
if (!append) {
|
||||||
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
|
}
|
||||||
|
val keyToProfile = batchSaveConfigs(configs, subid, append)
|
||||||
|
restoreOldPingData(keyToProfile, oldPingData)
|
||||||
|
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
||||||
|
matchKey?.let { MmkvManager.setSelectServer(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch save all parsed configs (only one serverList read/write)
|
return@synchronized configs.size
|
||||||
if (configs.isNotEmpty()) {
|
} catch (e: Exception) {
|
||||||
if (!append) {
|
Log.e(AppConfig.TAG, "Failed to parse batch config", e)
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
|
||||||
}
|
|
||||||
val keyToProfile = batchSaveConfigs(configs, subid)
|
|
||||||
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
|
||||||
matchKey?.let { MmkvManager.setSelectServer(it) }
|
|
||||||
}
|
}
|
||||||
|
return@synchronized 0
|
||||||
return configs.size
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to parse batch config", e)
|
|
||||||
}
|
}
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -270,15 +284,23 @@ object AngConfigManager {
|
|||||||
* @param subid The subscription ID.
|
* @param subid The subscription ID.
|
||||||
* @return Map of generated keys to their corresponding ProfileItem.
|
* @return Map of generated keys to their corresponding ProfileItem.
|
||||||
*/
|
*/
|
||||||
private fun batchSaveConfigs(configs: List<ProfileItem>, subid: String): Map<String, ProfileItem> {
|
private fun batchSaveConfigs(configs: List<ProfileItem>, subid: String, append: Boolean): Map<String, ProfileItem> {
|
||||||
val keyToProfile = mutableMapOf<String, ProfileItem>()
|
val keyToProfile = mutableMapOf<String, ProfileItem>()
|
||||||
|
|
||||||
val serverList = MmkvManager.decodeServerList(subid)
|
val serverList = if (append) {
|
||||||
|
MmkvManager.decodeServerList(subid)
|
||||||
|
} else {
|
||||||
|
mutableListOf()
|
||||||
|
}
|
||||||
var needSetSelected = MmkvManager.getSelectServer().isNullOrBlank()
|
var needSetSelected = MmkvManager.getSelectServer().isNullOrBlank()
|
||||||
|
|
||||||
val existingProfiles = serverList.mapNotNull { guid ->
|
val existingProfiles = if (append) {
|
||||||
MmkvManager.decodeServerConfig(guid)?.let { guid to it }
|
serverList.mapNotNull { guid ->
|
||||||
}.toMap()
|
MmkvManager.decodeServerConfig(guid)?.let { guid to it }
|
||||||
|
}.toMap()
|
||||||
|
} else {
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
configs.forEach { config ->
|
configs.forEach { config ->
|
||||||
val existingKey = existingProfiles.entries.firstOrNull { (_, existing) ->
|
val existingKey = existingProfiles.entries.firstOrNull { (_, existing) ->
|
||||||
@@ -286,6 +308,7 @@ object AngConfigManager {
|
|||||||
}?.key
|
}?.key
|
||||||
|
|
||||||
if (existingKey != null) {
|
if (existingKey != null) {
|
||||||
|
MmkvManager.encodeProfileDirect(existingKey, JsonUtil.toJson(config))
|
||||||
keyToProfile[existingKey] = config
|
keyToProfile[existingKey] = config
|
||||||
} else {
|
} else {
|
||||||
val key = Utils.getUuid()
|
val key = Utils.getUuid()
|
||||||
@@ -306,6 +329,35 @@ object AngConfigManager {
|
|||||||
return keyToProfile
|
return keyToProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveOldPingData(subid: String): Map<ProfileItem, Long> {
|
||||||
|
val pingData = mutableMapOf<ProfileItem, Long>()
|
||||||
|
val serverList = MmkvManager.decodeServerList(subid)
|
||||||
|
|
||||||
|
serverList.forEach { guid ->
|
||||||
|
val profile = MmkvManager.decodeServerConfig(guid)
|
||||||
|
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||||
|
if (profile != null && aff != null && aff.testDelayMillis > 0) {
|
||||||
|
pingData[profile] = aff.testDelayMillis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pingData
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreOldPingData(keyToProfile: Map<String, ProfileItem>, oldPingData: Map<ProfileItem, Long>) {
|
||||||
|
if (oldPingData.isEmpty()) return
|
||||||
|
|
||||||
|
keyToProfile.forEach { (key, newProfile) ->
|
||||||
|
val oldPing = oldPingData.entries.firstOrNull { (oldProfile, _) ->
|
||||||
|
oldProfile == newProfile
|
||||||
|
}?.value
|
||||||
|
|
||||||
|
if (oldPing != null && oldPing > 0) {
|
||||||
|
MmkvManager.encodeServerTestDelayMillis(key, oldPing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds a matched profile key from the given key-profile map using multi-level matching.
|
* Finds a matched profile key from the given key-profile map using multi-level matching.
|
||||||
* Matching priority (from highest to lowest):
|
* Matching priority (from highest to lowest):
|
||||||
@@ -370,67 +422,68 @@ object AngConfigManager {
|
|||||||
* @return The number of configurations parsed.
|
* @return The number of configurations parsed.
|
||||||
*/
|
*/
|
||||||
private fun parseCustomConfigServer(server: String?, subid: String, append: Boolean): Int {
|
private fun parseCustomConfigServer(server: String?, subid: String, append: Boolean): Int {
|
||||||
if (server == null) {
|
return synchronized(getSubscriptionLock(subid)) {
|
||||||
return 0
|
if (server == null) {
|
||||||
}
|
return@synchronized 0
|
||||||
if (server.contains("inbounds")
|
}
|
||||||
&& server.contains("outbounds")
|
if (server.contains("inbounds")
|
||||||
&& server.contains("routing")
|
&& server.contains("outbounds")
|
||||||
) {
|
&& server.contains("routing")
|
||||||
try {
|
) {
|
||||||
val serverList: Array<Any> =
|
try {
|
||||||
JsonUtil.fromJson(server, Array<Any>::class.java) ?: arrayOf()
|
val serverList: Array<Any> =
|
||||||
|
JsonUtil.fromJson(server, Array<Any>::class.java) ?: arrayOf()
|
||||||
|
|
||||||
if (serverList.isNotEmpty()) {
|
if (serverList.isNotEmpty()) {
|
||||||
|
if (!append) {
|
||||||
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
|
}
|
||||||
|
var count = 0
|
||||||
|
for (srv in serverList.reversed()) {
|
||||||
|
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
||||||
|
config.subscriptionId = subid
|
||||||
|
config.description = generateDescription(config)
|
||||||
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
|
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
return@synchronized count
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val config = CustomFmt.parse(server) ?: return@synchronized 0
|
||||||
|
config.subscriptionId = subid
|
||||||
|
config.description = generateDescription(config)
|
||||||
if (!append) {
|
if (!append) {
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
}
|
}
|
||||||
var count = 0
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
for (srv in serverList.reversed()) {
|
MmkvManager.encodeServerRaw(key, server)
|
||||||
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
return@synchronized 1
|
||||||
config.subscriptionId = subid
|
} catch (e: Exception) {
|
||||||
config.description = generateDescription(config)
|
Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
}
|
||||||
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
return@synchronized 0
|
||||||
count += 1
|
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
||||||
|
try {
|
||||||
|
val config = WireguardFmt.parseWireguardConfFile(server) ?: return@synchronized R.string.toast_incorrect_protocol
|
||||||
|
config.description = generateDescription(config)
|
||||||
|
if (!append) {
|
||||||
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
}
|
}
|
||||||
return count
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
|
MmkvManager.encodeServerRaw(key, server)
|
||||||
|
return@synchronized 1
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
return@synchronized 0
|
||||||
Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
|
} else {
|
||||||
|
return@synchronized 0
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
// For compatibility
|
|
||||||
val config = CustomFmt.parse(server) ?: return 0
|
|
||||||
config.subscriptionId = subid
|
|
||||||
config.description = generateDescription(config)
|
|
||||||
if (!append) {
|
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
|
||||||
}
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
|
||||||
return 1
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
|
||||||
try {
|
|
||||||
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
|
|
||||||
config.description = generateDescription(config)
|
|
||||||
if (!append) {
|
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
|
||||||
}
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
|
||||||
return 1
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,16 +595,18 @@ object AngConfigManager {
|
|||||||
Log.i(AppConfig.TAG, url)
|
Log.i(AppConfig.TAG, url)
|
||||||
val userAgent = it.subscription.userAgent
|
val userAgent = it.subscription.userAgent
|
||||||
|
|
||||||
|
val timeout = if (url.startsWith("https://key.zarazaex.xyz/sub")) 3000 else 15000
|
||||||
|
|
||||||
var configText = try {
|
var configText = try {
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort)
|
HttpUtil.getUrlContentWithUserAgent(url, userAgent, timeout, httpPort)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
if (configText.isEmpty()) {
|
if (configText.isEmpty()) {
|
||||||
configText = try {
|
configText = try {
|
||||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent)
|
HttpUtil.getUrlContentWithUserAgent(url, userAgent, timeout)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
||||||
""
|
""
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ object SettingsManager {
|
|||||||
*/
|
*/
|
||||||
fun getVpnDnsServers(): List<String> {
|
fun getVpnDnsServers(): List<String> {
|
||||||
val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
|
val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
|
||||||
return vpnDns.split(",").filter { it.isNotBlank() }
|
return vpnDns.split(",").filter { it.isNotBlank() && Utils.isPureIpAddress(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -94,18 +94,23 @@ object SpeedtestManager {
|
|||||||
var result: String
|
var result: String
|
||||||
var elapsed = -1L
|
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 {
|
try {
|
||||||
val start = SystemClock.elapsedRealtime()
|
val start = SystemClock.elapsedRealtime()
|
||||||
val code = conn.responseCode
|
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
|
elapsed = SystemClock.elapsedRealtime() - start
|
||||||
|
|
||||||
result = when (code) {
|
if (xyz.zarazaex.olc.util.Utils.isPureIpAddress(responseBody)) {
|
||||||
204 -> context.getString(R.string.connection_test_available, elapsed)
|
result = context.getString(R.string.connection_test_available, elapsed)
|
||||||
200 if conn.contentLengthLong == 0L -> context.getString(R.string.connection_test_available, elapsed)
|
} else {
|
||||||
else -> throw IOException(
|
throw IOException("Invalid IP response: $responseBody")
|
||||||
context.getString(R.string.connection_test_error_status_code, code)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(AppConfig.TAG, "Connection test IOException", e)
|
Log.e(AppConfig.TAG, "Connection test IOException", e)
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ object V2RayServiceManager {
|
|||||||
private val coreController: CoreController = V2RayNativeManager.newCoreController(CoreCallback())
|
private val coreController: CoreController = V2RayNativeManager.newCoreController(CoreCallback())
|
||||||
private val mMsgReceive = ReceiveMessageHandler()
|
private val mMsgReceive = ReceiveMessageHandler()
|
||||||
private var currentConfig: ProfileItem? = null
|
private var currentConfig: ProfileItem? = null
|
||||||
|
private val operationLock = Any()
|
||||||
|
@Volatile private var isOperationInProgress = false
|
||||||
|
|
||||||
var serviceControl: SoftReference<ServiceControl>? = null
|
var serviceControl: SoftReference<ServiceControl>? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -57,13 +59,27 @@ object V2RayServiceManager {
|
|||||||
* @param guid The GUID of the server configuration to use (optional).
|
* @param guid The GUID of the server configuration to use (optional).
|
||||||
*/
|
*/
|
||||||
fun startVService(context: Context, guid: String? = null) {
|
fun startVService(context: Context, guid: String? = null) {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}")
|
synchronized(operationLock) {
|
||||||
|
if (isOperationInProgress) {
|
||||||
if (guid != null) {
|
Log.w(AppConfig.TAG, "StartCore-Manager: Operation already in progress")
|
||||||
MmkvManager.setSelectServer(guid)
|
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,26 @@ object V2RayServiceManager {
|
|||||||
* @param context The context from which the service is stopped.
|
* @param context The context from which the service is stopped.
|
||||||
*/
|
*/
|
||||||
fun stopVService(context: Context) {
|
fun stopVService(context: Context) {
|
||||||
//context.toast(R.string.toast_services_stop)
|
synchronized(operationLock) {
|
||||||
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
|
if (isOperationInProgress) {
|
||||||
|
Log.w(AppConfig.TAG, "StartCore-Manager: Operation already in progress")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isOperationInProgress = true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (serviceControl?.get() == null) {
|
||||||
|
Log.w(AppConfig.TAG, "StartCore-Manager: Service not running, resetting UI state")
|
||||||
|
MessageUtil.sendMsg2UI(context, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
|
||||||
|
} finally {
|
||||||
|
synchronized(operationLock) {
|
||||||
|
isOperationInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -385,11 +419,17 @@ object V2RayServiceManager {
|
|||||||
|
|
||||||
AppConfig.MSG_STATE_STOP -> {
|
AppConfig.MSG_STATE_STOP -> {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: Stop service")
|
Log.i(AppConfig.TAG, "StartCore-Manager: Stop service")
|
||||||
|
synchronized(operationLock) {
|
||||||
|
isOperationInProgress = false
|
||||||
|
}
|
||||||
serviceControl.stopService()
|
serviceControl.stopService()
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_RESTART -> {
|
AppConfig.MSG_STATE_RESTART -> {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: Restart service")
|
Log.i(AppConfig.TAG, "StartCore-Manager: Restart service")
|
||||||
|
synchronized(operationLock) {
|
||||||
|
isOperationInProgress = false
|
||||||
|
}
|
||||||
serviceControl.stopService()
|
serviceControl.stopService()
|
||||||
Thread.sleep(500L)
|
Thread.sleep(500L)
|
||||||
startVService(serviceControl.getService())
|
startVService(serviceControl.getService())
|
||||||
|
|||||||
@@ -594,7 +594,6 @@ object V2rayConfigManager {
|
|||||||
|
|
||||||
val remoteDns = SettingsManager.getRemoteDnsServers()
|
val remoteDns = SettingsManager.getRemoteDnsServers()
|
||||||
val domesticDns = SettingsManager.getDomesticDnsServers()
|
val domesticDns = SettingsManager.getDomesticDnsServers()
|
||||||
val vpnDns = if (SettingsManager.isVpnMode()) SettingsManager.getVpnDnsServers() else emptyList()
|
|
||||||
val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY)
|
val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY)
|
||||||
val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT)
|
val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT)
|
||||||
val isCnRoutingMode = directDomain.contains(AppConfig.GEOSITE_CN)
|
val isCnRoutingMode = directDomain.contains(AppConfig.GEOSITE_CN)
|
||||||
@@ -621,14 +620,8 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vpnDns.isNotEmpty()) {
|
remoteDns.forEach {
|
||||||
vpnDns.forEach {
|
servers.add(it)
|
||||||
servers.add(it)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
remoteDns.forEach {
|
|
||||||
servers.add(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED)
|
val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED)
|
||||||
@@ -1318,7 +1311,7 @@ object V2rayConfigManager {
|
|||||||
if (start != null && end != null) {
|
if (start != null && end != null) {
|
||||||
val minStart = maxOf(5, start)
|
val minStart = maxOf(5, start)
|
||||||
val minEnd = maxOf(minStart, end)
|
val minEnd = maxOf(minStart, end)
|
||||||
"$minStart-$minEnd"
|
"$minStart-$minEnd"
|
||||||
} else {
|
} else {
|
||||||
"30"
|
"30"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import xyz.zarazaex.olc.handler.MmkvManager
|
|||||||
import xyz.zarazaex.olc.handler.NotificationManager
|
import xyz.zarazaex.olc.handler.NotificationManager
|
||||||
import xyz.zarazaex.olc.handler.SettingsManager
|
import xyz.zarazaex.olc.handler.SettingsManager
|
||||||
import xyz.zarazaex.olc.handler.V2RayServiceManager
|
import xyz.zarazaex.olc.handler.V2RayServiceManager
|
||||||
|
import xyz.zarazaex.olc.util.MessageUtil
|
||||||
import xyz.zarazaex.olc.util.MyContextWrapper
|
import xyz.zarazaex.olc.util.MyContextWrapper
|
||||||
import xyz.zarazaex.olc.util.Utils
|
import xyz.zarazaex.olc.util.Utils
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
@@ -94,6 +95,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
Log.i(AppConfig.TAG, "StartCore-VPN: Service destroyed")
|
Log.i(AppConfig.TAG, "StartCore-VPN: Service destroyed")
|
||||||
NotificationManager.cancelNotification()
|
NotificationManager.cancelNotification()
|
||||||
|
MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
|||||||
@@ -42,9 +42,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelectedListener {
|
class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelectedListener {
|
||||||
private val binding by lazy {
|
private val binding by lazy {ActivityMainBinding.inflate(layoutInflater)}
|
||||||
ActivityMainBinding.inflate(layoutInflater)
|
|
||||||
}
|
|
||||||
private var isLiteTesting = false
|
private var isLiteTesting = false
|
||||||
private var easterEggClickCount = 0
|
private var easterEggClickCount = 0
|
||||||
private var isEasterEggActive = false
|
private var isEasterEggActive = false
|
||||||
@@ -52,6 +50,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
val mainViewModel: MainViewModel by viewModels()
|
val mainViewModel: MainViewModel by viewModels()
|
||||||
private lateinit var groupPagerAdapter: GroupPagerAdapter
|
private lateinit var groupPagerAdapter: GroupPagerAdapter
|
||||||
private var tabMediator: TabLayoutMediator? = null
|
private var tabMediator: TabLayoutMediator? = null
|
||||||
|
@Volatile private var isFabOperationInProgress = false
|
||||||
|
|
||||||
private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
if (it.resultCode == RESULT_OK) {
|
if (it.resultCode == RESULT_OK) {
|
||||||
@@ -201,12 +200,24 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFabAction() {
|
private fun handleFabAction() {
|
||||||
|
if (isFabOperationInProgress) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isFabOperationInProgress = true
|
||||||
|
|
||||||
applyRunningState(isLoading = true, isRunning = false)
|
applyRunningState(isLoading = true, isRunning = false)
|
||||||
|
|
||||||
if (mainViewModel.isRunning.value == true) {
|
lifecycleScope.launch {
|
||||||
V2RayServiceManager.stopVService(this)
|
try {
|
||||||
} else {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
startV2RayWithPermission()
|
V2RayServiceManager.stopVService(this@MainActivity)
|
||||||
|
} else {
|
||||||
|
startV2RayWithPermission()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
delay(1000)
|
||||||
|
isFabOperationInProgress = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,29 +231,42 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLiteAction() {
|
private fun handleLiteAction() {
|
||||||
if (mainViewModel.isRunning.value == true) {
|
if (isFabOperationInProgress) {
|
||||||
V2RayServiceManager.stopVService(this)
|
return
|
||||||
}
|
}
|
||||||
|
isFabOperationInProgress = true
|
||||||
|
|
||||||
showStatus("Обновление профилей...")
|
lifecycleScope.launch {
|
||||||
showLoading()
|
try {
|
||||||
isLiteTesting = true
|
if (mainViewModel.isRunning.value == true) {
|
||||||
|
V2RayServiceManager.stopVService(this@MainActivity)
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
delay(1000)
|
||||||
val result = mainViewModel.updateConfigViaSubAll()
|
|
||||||
delay(500L)
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
if (result.configCount > 0) {
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
showStatus("Обновлено ${result.configCount} профилей. Запуск теста...")
|
|
||||||
} else {
|
|
||||||
showStatus("Запуск теста...")
|
|
||||||
}
|
}
|
||||||
hideLoading()
|
|
||||||
|
|
||||||
delay(500L)
|
showStatus("Обновление профилей...")
|
||||||
showStatus("Выполняется замер задержки. Ожидаем завершения...")
|
showLoading()
|
||||||
mainViewModel.testAllRealPing()
|
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() {
|
fun restartV2Ray() {
|
||||||
if (mainViewModel.isRunning.value == true) {
|
if (isFabOperationInProgress) {
|
||||||
V2RayServiceManager.stopVService(this)
|
return
|
||||||
}
|
}
|
||||||
|
isFabOperationInProgress = true
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
delay(500)
|
try {
|
||||||
startV2Ray()
|
if (mainViewModel.isRunning.value == true) {
|
||||||
|
V2RayServiceManager.stopVService(this@MainActivity)
|
||||||
|
}
|
||||||
|
delay(1000)
|
||||||
|
startV2Ray()
|
||||||
|
} finally {
|
||||||
|
delay(500)
|
||||||
|
isFabOperationInProgress = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
//setContentView(binding.root)
|
|
||||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_sub_setting))
|
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_sub_setting))
|
||||||
|
|
||||||
adapter = SubSettingRecyclerAdapter(viewModel, ActivityAdapterListener())
|
adapter = SubSettingRecyclerAdapter(viewModel, ActivityAdapterListener())
|
||||||
@@ -53,6 +52,14 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
|
|
||||||
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
||||||
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
||||||
|
|
||||||
|
viewModel.isUpdating.observe(this) { isUpdating ->
|
||||||
|
adapter.setUpdating(isUpdating)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.subscriptionStatuses.observe(this) { statuses ->
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -62,44 +69,105 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.action_sub_setting, menu)
|
menuInflater.inflate(R.menu.action_sub_setting, menu)
|
||||||
|
viewModel.isUpdating.observe(this) { isUpdating ->
|
||||||
|
menu.findItem(R.id.sub_update)?.isEnabled = !isUpdating
|
||||||
|
menu.findItem(R.id.add_config)?.isEnabled = !isUpdating
|
||||||
|
}
|
||||||
return super.onCreateOptionsMenu(menu)
|
return super.onCreateOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
R.id.add_config -> {
|
return when (item.itemId) {
|
||||||
startActivity(Intent(this, SubEditActivity::class.java))
|
R.id.add_config -> {
|
||||||
true
|
startActivity(Intent(this, SubEditActivity::class.java))
|
||||||
}
|
true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.sub_update -> {
|
R.id.sub_update -> {
|
||||||
showLoading()
|
if (viewModel.isUpdating.value == true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
showLoading()
|
||||||
val result = AngConfigManager.updateConfigViaSubAll()
|
viewModel.isUpdating.value = true
|
||||||
delay(500L)
|
viewModel.subscriptionStatuses.value = emptyMap()
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
if (result.successCount + result.failureCount + result.skipCount == 0) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
val subscriptions = viewModel.getAll()
|
||||||
|
var totalConfigCount = 0
|
||||||
|
var successCount = 0
|
||||||
|
var failureCount = 0
|
||||||
|
var skipCount = 0
|
||||||
|
|
||||||
|
val jobs = subscriptions.map { subscription ->
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
val subId = subscription.guid
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
viewModel.updateSubscriptionStatus(subId, xyz.zarazaex.olc.dto.SubscriptionUpdateStatus.LOADING)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = AngConfigManager.updateConfigViaSub(subscription)
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
when {
|
||||||
|
result.successCount > 0 -> {
|
||||||
|
viewModel.updateSubscriptionStatus(
|
||||||
|
subId,
|
||||||
|
xyz.zarazaex.olc.dto.SubscriptionUpdateStatus.SUCCESS,
|
||||||
|
result.configCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result.skipCount > 0 -> {
|
||||||
|
viewModel.updateSubscriptionStatus(
|
||||||
|
subId,
|
||||||
|
xyz.zarazaex.olc.dto.SubscriptionUpdateStatus.SKIPPED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
viewModel.updateSubscriptionStatus(
|
||||||
|
subId,
|
||||||
|
xyz.zarazaex.olc.dto.SubscriptionUpdateStatus.FAILED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(this@SubSettingActivity) {
|
||||||
|
totalConfigCount += result.configCount
|
||||||
|
successCount += result.successCount
|
||||||
|
failureCount += result.failureCount
|
||||||
|
skipCount += result.skipCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs.forEach { it.join() }
|
||||||
|
|
||||||
|
delay(500L)
|
||||||
|
viewModel.isUpdating.value = false
|
||||||
|
|
||||||
|
if (successCount + failureCount + skipCount == 0) {
|
||||||
toast(R.string.title_update_subscription_no_subscription)
|
toast(R.string.title_update_subscription_no_subscription)
|
||||||
} else if (result.successCount > 0 && result.failureCount + result.skipCount == 0) {
|
} else if (successCount > 0 && failureCount + skipCount == 0) {
|
||||||
toast(getString(R.string.title_update_config_count, result.configCount))
|
toast(getString(R.string.title_update_config_count, totalConfigCount))
|
||||||
} else {
|
} else {
|
||||||
toast(
|
toast(
|
||||||
getString(
|
getString(
|
||||||
R.string.title_update_subscription_result,
|
R.string.title_update_subscription_result,
|
||||||
result.configCount, result.successCount, result.failureCount, result.skipCount
|
totalConfigCount, successCount, failureCount, skipCount
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
hideLoading()
|
hideLoading()
|
||||||
refreshData()
|
refreshData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import android.view.ViewGroup
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import xyz.zarazaex.olc.AppConfig
|
import xyz.zarazaex.olc.AppConfig
|
||||||
|
import xyz.zarazaex.olc.R
|
||||||
import xyz.zarazaex.olc.contracts.BaseAdapterListener
|
import xyz.zarazaex.olc.contracts.BaseAdapterListener
|
||||||
import xyz.zarazaex.olc.databinding.ItemRecyclerSubSettingBinding
|
import xyz.zarazaex.olc.databinding.ItemRecyclerSubSettingBinding
|
||||||
|
import xyz.zarazaex.olc.dto.SubscriptionUpdateStatus
|
||||||
import xyz.zarazaex.olc.helper.ItemTouchHelperAdapter
|
import xyz.zarazaex.olc.helper.ItemTouchHelperAdapter
|
||||||
import xyz.zarazaex.olc.helper.ItemTouchHelperViewHolder
|
import xyz.zarazaex.olc.helper.ItemTouchHelperViewHolder
|
||||||
import xyz.zarazaex.olc.util.Utils
|
import xyz.zarazaex.olc.util.Utils
|
||||||
@@ -20,6 +22,15 @@ class SubSettingRecyclerAdapter(
|
|||||||
private val adapterListener: BaseAdapterListener?
|
private val adapterListener: BaseAdapterListener?
|
||||||
) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter {
|
) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter {
|
||||||
|
|
||||||
|
private var isUpdating = false
|
||||||
|
|
||||||
|
fun setUpdating(updating: Boolean) {
|
||||||
|
if (isUpdating != updating) {
|
||||||
|
isUpdating = updating
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getItemCount() = viewModel.getAll().size
|
override fun getItemCount() = viewModel.getAll().size
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||||
@@ -32,16 +43,60 @@ class SubSettingRecyclerAdapter(
|
|||||||
holder.itemSubSettingBinding.tvLastUpdated.text = Utils.formatTimestamp(subItem.lastUpdated)
|
holder.itemSubSettingBinding.tvLastUpdated.text = Utils.formatTimestamp(subItem.lastUpdated)
|
||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
|
val subStatus = viewModel.getSubscriptionStatus(subId)
|
||||||
|
when (subStatus?.status) {
|
||||||
|
SubscriptionUpdateStatus.LOADING -> {
|
||||||
|
holder.itemSubSettingBinding.progressBar.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.text = holder.itemView.context.getString(R.string.title_updating)
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.setTextColor(Color.GRAY)
|
||||||
|
}
|
||||||
|
SubscriptionUpdateStatus.SUCCESS -> {
|
||||||
|
holder.itemSubSettingBinding.progressBar.visibility = View.GONE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.text = "✓ ${subStatus.configCount}"
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.setTextColor(Color.parseColor("#4CAF50"))
|
||||||
|
}
|
||||||
|
SubscriptionUpdateStatus.FAILED -> {
|
||||||
|
holder.itemSubSettingBinding.progressBar.visibility = View.GONE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.text = "✗"
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.setTextColor(Color.parseColor("#F44336"))
|
||||||
|
}
|
||||||
|
SubscriptionUpdateStatus.SKIPPED -> {
|
||||||
|
holder.itemSubSettingBinding.progressBar.visibility = View.GONE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.text = "—"
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.setTextColor(Color.GRAY)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
holder.itemSubSettingBinding.progressBar.visibility = View.GONE
|
||||||
|
holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isEnabled = !isUpdating
|
||||||
|
|
||||||
|
holder.itemSubSettingBinding.layoutEdit.isClickable = isEnabled
|
||||||
|
holder.itemSubSettingBinding.layoutEdit.alpha = if (isEnabled) 1.0f else 0.5f
|
||||||
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
||||||
adapterListener?.onEdit(subId, position)
|
if (isEnabled) {
|
||||||
|
adapterListener?.onEdit(subId, position)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.itemSubSettingBinding.layoutRemove.isClickable = isEnabled
|
||||||
|
holder.itemSubSettingBinding.layoutRemove.alpha = if (isEnabled) 1.0f else 0.5f
|
||||||
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
||||||
adapterListener?.onRemove(subId, position)
|
if (isEnabled) {
|
||||||
|
adapterListener?.onRemove(subId, position)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.itemSubSettingBinding.chkEnable.isEnabled = isEnabled
|
||||||
|
holder.itemSubSettingBinding.chkEnable.alpha = if (isEnabled) 1.0f else 0.5f
|
||||||
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
||||||
if (!it.isPressed) return@setOnCheckedChangeListener
|
if (!it.isPressed || !isEnabled) return@setOnCheckedChangeListener
|
||||||
subItem.enabled = isChecked
|
subItem.enabled = isChecked
|
||||||
viewModel.update(subId, subItem)
|
viewModel.update(subId, subItem)
|
||||||
}
|
}
|
||||||
@@ -56,8 +111,12 @@ class SubSettingRecyclerAdapter(
|
|||||||
holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE
|
holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE
|
||||||
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
|
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
|
||||||
holder.itemSubSettingBinding.layoutLastUpdated.visibility = View.VISIBLE
|
holder.itemSubSettingBinding.layoutLastUpdated.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.layoutShare.isClickable = isEnabled
|
||||||
|
holder.itemSubSettingBinding.layoutShare.alpha = if (isEnabled) 1.0f else 0.5f
|
||||||
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
||||||
adapterListener?.onShare(subItem.url)
|
if (isEnabled) {
|
||||||
|
adapterListener?.onShare(subItem.url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,6 +266,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
updateListAction.value = -1
|
updateListAction.value = -1
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
if (serversCache.isEmpty()) {
|
||||||
|
withContext(Dispatchers.Main) { reloadServerList() }
|
||||||
|
}
|
||||||
if (serversCache.isEmpty()) {
|
if (serversCache.isEmpty()) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package xyz.zarazaex.olc.viewmodel
|
package xyz.zarazaex.olc.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import xyz.zarazaex.olc.dto.SubscriptionCache
|
import xyz.zarazaex.olc.dto.SubscriptionCache
|
||||||
import xyz.zarazaex.olc.dto.SubscriptionItem
|
import xyz.zarazaex.olc.dto.SubscriptionItem
|
||||||
|
import xyz.zarazaex.olc.dto.SubscriptionStatus
|
||||||
|
import xyz.zarazaex.olc.dto.SubscriptionUpdateStatus
|
||||||
import xyz.zarazaex.olc.handler.MmkvManager
|
import xyz.zarazaex.olc.handler.MmkvManager
|
||||||
import xyz.zarazaex.olc.handler.SettingsChangeManager
|
import xyz.zarazaex.olc.handler.SettingsChangeManager
|
||||||
import xyz.zarazaex.olc.handler.SettingsManager
|
import xyz.zarazaex.olc.handler.SettingsManager
|
||||||
@@ -11,11 +14,25 @@ class SubscriptionsViewModel : ViewModel() {
|
|||||||
private val subscriptions: MutableList<SubscriptionCache> =
|
private val subscriptions: MutableList<SubscriptionCache> =
|
||||||
MmkvManager.decodeSubscriptions().toMutableList()
|
MmkvManager.decodeSubscriptions().toMutableList()
|
||||||
|
|
||||||
|
val isUpdating = MutableLiveData<Boolean>(false)
|
||||||
|
val subscriptionStatuses = MutableLiveData<Map<String, SubscriptionStatus>>(emptyMap())
|
||||||
|
|
||||||
fun getAll(): List<SubscriptionCache> = subscriptions.toList()
|
fun getAll(): List<SubscriptionCache> = subscriptions.toList()
|
||||||
|
|
||||||
fun reload() {
|
fun reload() {
|
||||||
subscriptions.clear()
|
subscriptions.clear()
|
||||||
subscriptions.addAll(MmkvManager.decodeSubscriptions())
|
subscriptions.addAll(MmkvManager.decodeSubscriptions())
|
||||||
|
subscriptionStatuses.value = emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSubscriptionStatus(guid: String, status: SubscriptionUpdateStatus, configCount: Int = 0) {
|
||||||
|
val currentStatuses = subscriptionStatuses.value?.toMutableMap() ?: mutableMapOf()
|
||||||
|
currentStatuses[guid] = SubscriptionStatus(guid, status, configCount)
|
||||||
|
subscriptionStatuses.postValue(currentStatuses)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSubscriptionStatus(guid: String): SubscriptionStatus? {
|
||||||
|
return subscriptionStatuses.value?.get(guid)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(subId: String): Boolean {
|
fun remove(subId: String): Boolean {
|
||||||
|
|||||||
@@ -168,9 +168,26 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_last_updated"
|
android:id="@+id/tv_last_updated"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Caption"/>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_bar"
|
||||||
|
style="?android:attr/progressBarStyleSmall"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_marginStart="@dimen/padding_spacing_dp8"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_update_status"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Caption"/>
|
android:layout_marginStart="@dimen/padding_spacing_dp8"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="color_fab_active">#88AC8A</color>
|
<color name="color_fab_active">@android:color/system_accent1_400</color>
|
||||||
<color name="color_fab_inactive">#646464</color>
|
<color name="color_fab_inactive">#646464</color>
|
||||||
<color name="divider_color_light">#424242</color>
|
<color name="divider_color_light">#424242</color>
|
||||||
|
|
||||||
<!-- Primary colors - main tone: gray -->
|
|
||||||
<color name="md_theme_primary">#C0C0C0</color>
|
<color name="md_theme_primary">#C0C0C0</color>
|
||||||
<color name="md_theme_onPrimary">#303030</color>
|
<color name="md_theme_onPrimary">#303030</color>
|
||||||
<color name="md_theme_primaryContainer">#474747</color>
|
<color name="md_theme_primaryContainer">#474747</color>
|
||||||
<color name="md_theme_onPrimaryContainer">#E0E0E0</color>
|
<color name="md_theme_onPrimaryContainer">#E0E0E0</color>
|
||||||
|
|
||||||
<!-- Secondary colors - accent color: green -->
|
<color name="md_theme_secondary">@android:color/system_accent1_400</color>
|
||||||
<color name="md_theme_secondary">#88AC8A</color>
|
|
||||||
<color name="md_theme_onSecondary">#FFFFFF</color>
|
<color name="md_theme_onSecondary">#FFFFFF</color>
|
||||||
<color name="md_theme_secondaryContainer">#6F3800</color>
|
<color name="md_theme_secondaryContainer">#6F3800</color>
|
||||||
<color name="md_theme_onSecondaryContainer">#FFE8D6</color>
|
<color name="md_theme_onSecondaryContainer">#FFE8D6</color>
|
||||||
|
|
||||||
<!-- Tertiary colors - tertiary color: green -->
|
<color name="md_theme_tertiary">@android:color/system_accent1_300</color>
|
||||||
<color name="md_theme_tertiary">#83D6B5</color>
|
|
||||||
<color name="md_theme_onTertiary">#00382E</color>
|
<color name="md_theme_onTertiary">#00382E</color>
|
||||||
<color name="md_theme_tertiaryContainer">#005143</color>
|
<color name="md_theme_tertiaryContainer">#005143</color>
|
||||||
<color name="md_theme_onTertiaryContainer">#A0F2D0</color>
|
<color name="md_theme_onTertiaryContainer">@android:color/system_accent1_200</color>
|
||||||
|
|
||||||
<!-- Error colors -->
|
<!-- Error colors -->
|
||||||
<color name="md_theme_error">#FFB4AB</color>
|
<color name="md_theme_error">#FFB4AB</color>
|
||||||
|
|||||||
@@ -298,6 +298,7 @@
|
|||||||
<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_updating">Обновление…</string>
|
||||||
<string name="title_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$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="title_update_subscription_no_subscription">Нет подписок</string>
|
||||||
<string name="toast_server_not_found_in_group">Выбранный профиль не найден в текущей группе</string>
|
<string name="toast_server_not_found_in_group">Выбранный профиль не найден в текущей группе</string>
|
||||||
|
|||||||
@@ -1,30 +1,27 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="colorPing">#009966</color>
|
<color name="colorPing">@android:color/system_accent1_600</color>
|
||||||
<color name="colorPingRed">#FF0099</color>
|
<color name="colorPingRed">#FF0099</color>
|
||||||
<color name="colorConfigType">#88AC8A</color>
|
<color name="colorConfigType">@android:color/system_accent1_400</color>
|
||||||
<color name="colorWhite">#FFFFFF</color>
|
<color name="colorWhite">#FFFFFF</color>
|
||||||
<color name="color_fab_active">#88AC8A</color>
|
<color name="color_fab_active">@android:color/system_accent1_400</color>
|
||||||
<color name="color_fab_inactive">#9C9C9C</color>
|
<color name="color_fab_inactive">#9C9C9C</color>
|
||||||
<color name="divider_color_light">#E0E0E0</color>
|
<color name="divider_color_light">#E0E0E0</color>
|
||||||
<color name="colorIndicator">@color/md_theme_primary</color>
|
<color name="colorIndicator">@color/md_theme_primary</color>
|
||||||
|
|
||||||
<!-- Primary colors - main tone: black -->
|
|
||||||
<color name="md_theme_primary">#000000</color>
|
<color name="md_theme_primary">#000000</color>
|
||||||
<color name="md_theme_onPrimary">#FFFFFF</color>
|
<color name="md_theme_onPrimary">#FFFFFF</color>
|
||||||
<color name="md_theme_primaryContainer">#E0E0E0</color>
|
<color name="md_theme_primaryContainer">#E0E0E0</color>
|
||||||
<color name="md_theme_onPrimaryContainer">#000000</color>
|
<color name="md_theme_onPrimaryContainer">#000000</color>
|
||||||
|
|
||||||
<!-- Secondary colors - accent color: green -->
|
<color name="md_theme_secondary">@android:color/system_accent1_400</color>
|
||||||
<color name="md_theme_secondary">#88AC8A</color>
|
|
||||||
<color name="md_theme_onSecondary">#FFFFFF</color>
|
<color name="md_theme_onSecondary">#FFFFFF</color>
|
||||||
<color name="md_theme_secondaryContainer">#FFE8D6</color>
|
<color name="md_theme_secondaryContainer">#FFE8D6</color>
|
||||||
<color name="md_theme_onSecondaryContainer">#2B1700</color>
|
<color name="md_theme_onSecondaryContainer">#2B1700</color>
|
||||||
|
|
||||||
<!-- Tertiary colors - accent color: green -->
|
<color name="md_theme_tertiary">@android:color/system_accent1_600</color>
|
||||||
<color name="md_theme_tertiary">#009966</color>
|
|
||||||
<color name="md_theme_onTertiary">#FFFFFF</color>
|
<color name="md_theme_onTertiary">#FFFFFF</color>
|
||||||
<color name="md_theme_tertiaryContainer">#A0F2D0</color>
|
<color name="md_theme_tertiaryContainer">@android:color/system_accent1_200</color>
|
||||||
<color name="md_theme_onTertiaryContainer">#00201A</color>
|
<color name="md_theme_onTertiaryContainer">#00201A</color>
|
||||||
|
|
||||||
<!-- Error colors -->
|
<!-- Error colors -->
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
<string name="navigation_drawer_open">Open navigation drawer</string>
|
<string name="navigation_drawer_open">Open navigation drawer</string>
|
||||||
<string name="navigation_drawer_close">Close navigation drawer</string>
|
<string name="navigation_drawer_close">Close navigation drawer</string>
|
||||||
<string name="migration_success">Data migration success!</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_forked_text" translatable="false">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_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="action_stop_service">Stop service</string>
|
||||||
<string name="migration_fail">Data migration failed!</string>
|
<string name="migration_fail">Data migration failed!</string>
|
||||||
<string name="pull_down_to_refresh">Please pull down to refresh!</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="summary_pref_tg_group">Join Telegram Group</string>
|
||||||
<string name="toast_tg_app_not_found">Telegram app not found</string>
|
<string name="toast_tg_app_not_found">Telegram app not found</string>
|
||||||
<string name="title_privacy_policy">Privacy policy</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_about">About</string>
|
||||||
<string name="title_source_code">Source code</string>
|
<string name="title_source_code">Source code</string>
|
||||||
<string name="title_oss_license">Open Source licenses</string>
|
<string name="title_oss_license">Open Source licenses</string>
|
||||||
@@ -304,6 +304,7 @@
|
|||||||
<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_updating">Updating…</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_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="title_update_subscription_no_subscription">No subscriptions</string>
|
||||||
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
|
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
|
||||||
|
|||||||
@@ -8,13 +8,11 @@
|
|||||||
<item name="colorPrimaryContainer">@color/md_theme_primaryContainer</item>
|
<item name="colorPrimaryContainer">@color/md_theme_primaryContainer</item>
|
||||||
<item name="colorOnPrimaryContainer">@color/md_theme_onPrimaryContainer</item>
|
<item name="colorOnPrimaryContainer">@color/md_theme_onPrimaryContainer</item>
|
||||||
|
|
||||||
<!-- Secondary colors - accent color: orange -->
|
|
||||||
<item name="colorSecondary">@color/md_theme_secondary</item>
|
<item name="colorSecondary">@color/md_theme_secondary</item>
|
||||||
<item name="colorOnSecondary">@color/md_theme_onSecondary</item>
|
<item name="colorOnSecondary">@color/md_theme_onSecondary</item>
|
||||||
<item name="colorSecondaryContainer">@color/md_theme_secondaryContainer</item>
|
<item name="colorSecondaryContainer">@color/md_theme_secondaryContainer</item>
|
||||||
<item name="colorOnSecondaryContainer">@color/md_theme_onSecondaryContainer</item>
|
<item name="colorOnSecondaryContainer">@color/md_theme_onSecondaryContainer</item>
|
||||||
|
|
||||||
<!-- Tertiary colors - tertiary color: green -->
|
|
||||||
<item name="colorTertiary">@color/md_theme_tertiary</item>
|
<item name="colorTertiary">@color/md_theme_tertiary</item>
|
||||||
<item name="colorOnTertiary">@color/md_theme_onTertiary</item>
|
<item name="colorOnTertiary">@color/md_theme_onTertiary</item>
|
||||||
<item name="colorTertiaryContainer">@color/md_theme_tertiaryContainer</item>
|
<item name="colorTertiaryContainer">@color/md_theme_tertiaryContainer</item>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
agp = "9.1.0"
|
agp = "9.1.0"
|
||||||
desugarJdkLibs = "2.1.5"
|
desugarJdkLibs = "2.1.5"
|
||||||
gradleLicensePlugin = "0.9.8"
|
gradleLicensePlugin = "0.9.8"
|
||||||
kotlin = "2.3.10"
|
kotlin = "2.1.0"
|
||||||
coreKtx = "1.17.0"
|
coreKtx = "1.17.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.3.0"
|
junitVersion = "1.3.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user