mirror of
https://github.com/openlibrecommunity/olcng.git
synced 2026-07-03 14:05:17 +02:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fea86944f | |||
| ff2fea8c03 | |||
| 415f2230e6 | |||
| fb827c1fb1 | |||
| cb4e3aab54 | |||
| 732d7248af | |||
| 0c32ddc642 | |||
| 153c545400 | |||
| 9af6472f8b | |||
| 346be42e37 | |||
| dd9e076ab1 | |||
| 903b0d4414 | |||
| f58a4f8f6f | |||
| fdce3ea2c1 | |||
| f0d620676d | |||
| b92a6cdfac | |||
| 45da2479dd | |||
| 1c936b2b31 | |||
| dbe109eedb | |||
| 7705aded77 | |||
| 4a2d62b671 | |||
| 4acca4e554 | |||
| b875613fb3 | |||
| d9d21061fa | |||
| fc7804fc1e | |||
| 4e9de615a4 | |||
| 7617ce898c | |||
| 8d284fd68a | |||
| 0f28310801 | |||
| a46123aeab | |||
| 06579df2a4 | |||
| 56532f10d3 | |||
| e843b92ef2 |
@@ -138,7 +138,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
chmod 755 gradlew
|
chmod 755 gradlew
|
||||||
./gradlew licenseFdroidReleaseReport
|
|
||||||
./gradlew assembleRelease --info 2>&1 | grep -i "signing\|keystore" || true
|
./gradlew assembleRelease --info 2>&1 | grep -i "signing\|keystore" || true
|
||||||
|
|
||||||
- name: Upload arm64-v8a APK
|
- name: Upload arm64-v8a APK
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
|
||||||
id("com.jaredsburrows.license")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -19,8 +17,6 @@ android {
|
|||||||
versionCode = envVersionCode?.toIntOrNull() ?: 717
|
versionCode = envVersionCode?.toIntOrNull() ?: 717
|
||||||
versionName = envVersionName ?: "2.0.17"
|
versionName = envVersionName ?: "2.0.17"
|
||||||
|
|
||||||
multiDexEnabled = true
|
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
@@ -90,7 +86,7 @@ android {
|
|||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("main") {
|
getByName("main") {
|
||||||
jniLibs.srcDirs("libs")
|
jniLibs.directories.add("libs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,50 +102,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationVariants.all {
|
|
||||||
val variant = this
|
|
||||||
val isFdroid = variant.productFlavors.any { it.name == "fdroid" }
|
|
||||||
if (isFdroid) {
|
|
||||||
val versionCodes =
|
|
||||||
mapOf(
|
|
||||||
"armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0
|
|
||||||
)
|
|
||||||
|
|
||||||
variant.outputs
|
|
||||||
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
|
||||||
.forEach { output ->
|
|
||||||
val abi = output.getFilter("ABI") ?: "universal"
|
|
||||||
output.outputFileName = "v2rayNG_${variant.versionName}-fdroid_${abi}.apk"
|
|
||||||
if (versionCodes.containsKey(abi)) {
|
|
||||||
output.versionCodeOverride =
|
|
||||||
(100 * variant.versionCode + versionCodes[abi]!!).plus(5000000)
|
|
||||||
} else {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val versionCodes =
|
|
||||||
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4)
|
|
||||||
|
|
||||||
variant.outputs
|
|
||||||
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
|
||||||
.forEach { output ->
|
|
||||||
val abi = if (output.getFilter("ABI") != null)
|
|
||||||
output.getFilter("ABI")
|
|
||||||
else
|
|
||||||
"universal"
|
|
||||||
|
|
||||||
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
|
|
||||||
if (versionCodes.containsKey(abi)) {
|
|
||||||
output.versionCodeOverride =
|
|
||||||
(1000000 * versionCodes[abi]!!).plus(variant.versionCode)
|
|
||||||
} else {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
@@ -163,6 +115,28 @@ android {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidComponents {
|
||||||
|
onVariants { variant ->
|
||||||
|
val isFdroid = variant.productFlavors.any { it.second == "fdroid" }
|
||||||
|
variant.outputs.forEach { output ->
|
||||||
|
val abi = output.filters.find {
|
||||||
|
it.filterType == com.android.build.api.variant.FilterConfiguration.FilterType.ABI
|
||||||
|
}?.identifier ?: "universal"
|
||||||
|
|
||||||
|
if (isFdroid) {
|
||||||
|
val versionCodes = mapOf(
|
||||||
|
"armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0
|
||||||
|
)
|
||||||
|
versionCodes[abi]?.let { code ->
|
||||||
|
output.versionCode.set((100 * (output.versionCode.get() ?: 0) + code) + 5000000)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.versionCode.set(1000000 * 4 + (output.versionCode.get() ?: 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Core Libraries
|
// Core Libraries
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
|
||||||
@@ -210,9 +184,6 @@ dependencies {
|
|||||||
implementation(libs.work.runtime.ktx)
|
implementation(libs.work.runtime.ktx)
|
||||||
implementation(libs.work.multiprocess)
|
implementation(libs.work.multiprocess)
|
||||||
|
|
||||||
// Multidex Support
|
|
||||||
implementation(libs.multidex)
|
|
||||||
|
|
||||||
// Testing Libraries
|
// Testing Libraries
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
|||||||
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.
Binary file not shown.
@@ -30,7 +30,8 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"destOverride": [
|
"destOverride": [
|
||||||
"http",
|
"http",
|
||||||
"tls"
|
"tls",
|
||||||
|
"quic"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"destOverride": [
|
"destOverride": [
|
||||||
"http",
|
"http",
|
||||||
"tls"
|
"tls",
|
||||||
|
"quic"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -47,7 +48,8 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"destOverride": [
|
"destOverride": [
|
||||||
"http",
|
"http",
|
||||||
"tls"
|
"tls",
|
||||||
|
"quic"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package xyz.zarazaex.olc
|
package xyz.zarazaex.olc
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.multidex.MultiDexApplication
|
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
@@ -9,7 +9,7 @@ import com.tencent.mmkv.MMKV
|
|||||||
import xyz.zarazaex.olc.AppConfig.ANG_PACKAGE
|
import xyz.zarazaex.olc.AppConfig.ANG_PACKAGE
|
||||||
import xyz.zarazaex.olc.handler.SettingsManager
|
import xyz.zarazaex.olc.handler.SettingsManager
|
||||||
|
|
||||||
class AngApplication : MultiDexApplication() {
|
class AngApplication : Application() {
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var application: AngApplication
|
lateinit var application: AngApplication
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,8 +123,7 @@ object AppConfig {
|
|||||||
// const val IP_API_URL = "https://speed.cloudflare.com/meta"
|
// const val IP_API_URL = "https://speed.cloudflare.com/meta"
|
||||||
const val IP_API_URL = "https://api.ip.sb/geoip"
|
const val IP_API_URL = "https://api.ip.sb/geoip"
|
||||||
|
|
||||||
/** DNS server addresses. */
|
const val DNS_PROXY = "https://1.1.1.1/dns-query"
|
||||||
const val DNS_PROXY = "1.1.1.1"
|
|
||||||
const val DNS_DIRECT = "223.5.5.5"
|
const val DNS_DIRECT = "223.5.5.5"
|
||||||
const val DNS_VPN = "1.1.1.1"
|
const val DNS_VPN = "1.1.1.1"
|
||||||
const val GEOSITE_PRIVATE = "geosite:private"
|
const val GEOSITE_PRIVATE = "geosite:private"
|
||||||
|
|||||||
@@ -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,33 +284,80 @@ 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>()
|
||||||
|
|
||||||
// Read serverList once
|
val serverList = if (append) {
|
||||||
val serverList = MmkvManager.decodeServerList(subid)
|
MmkvManager.decodeServerList(subid)
|
||||||
|
} else {
|
||||||
|
mutableListOf()
|
||||||
|
}
|
||||||
var needSetSelected = MmkvManager.getSelectServer().isNullOrBlank()
|
var needSetSelected = MmkvManager.getSelectServer().isNullOrBlank()
|
||||||
|
|
||||||
configs.forEach { config ->
|
val existingProfiles = if (append) {
|
||||||
val key = Utils.getUuid()
|
serverList.mapNotNull { guid ->
|
||||||
// Save profile directly without updating serverList
|
MmkvManager.decodeServerConfig(guid)?.let { guid to it }
|
||||||
MmkvManager.encodeProfileDirect(key, JsonUtil.toJson(config))
|
}.toMap()
|
||||||
|
} else {
|
||||||
if (!serverList.contains(key)) {
|
emptyMap()
|
||||||
serverList.add(0, key)
|
}
|
||||||
if (needSetSelected) {
|
|
||||||
MmkvManager.setSelectServer(key)
|
configs.forEach { config ->
|
||||||
needSetSelected = false
|
val existingKey = existingProfiles.entries.firstOrNull { (_, existing) ->
|
||||||
}
|
existing == config
|
||||||
}
|
}?.key
|
||||||
keyToProfile[key] = config
|
|
||||||
|
if (existingKey != null) {
|
||||||
|
MmkvManager.encodeProfileDirect(existingKey, JsonUtil.toJson(config))
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write serverList once
|
|
||||||
MmkvManager.encodeServerList(serverList, subid)
|
MmkvManager.encodeServerList(serverList, subid)
|
||||||
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):
|
||||||
@@ -361,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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,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 { Utils.isPureIpAddress(it) }
|
return vpnDns.split(",").filter { it.isNotBlank() && Utils.isPureIpAddress(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -438,7 +438,6 @@ object SettingsManager {
|
|||||||
* Ensure default settings are present in MMKV.
|
* Ensure default settings are present in MMKV.
|
||||||
*/
|
*/
|
||||||
private fun ensureDefaultSettings() {
|
private fun ensureDefaultSettings() {
|
||||||
// Write defaults in the exact order requested by the user
|
|
||||||
ensureDefaultValue(AppConfig.PREF_MODE, AppConfig.VPN)
|
ensureDefaultValue(AppConfig.PREF_MODE, AppConfig.VPN)
|
||||||
ensureDefaultValue(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
|
ensureDefaultValue(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
|
||||||
ensureDefaultValue(AppConfig.PREF_VPN_MTU, AppConfig.VPN_MTU.toString())
|
ensureDefaultValue(AppConfig.PREF_VPN_MTU, AppConfig.VPN_MTU.toString())
|
||||||
@@ -453,6 +452,9 @@ object SettingsManager {
|
|||||||
ensureDefaultValue(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
|
ensureDefaultValue(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
|
||||||
ensureDefaultValue(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
|
ensureDefaultValue(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
|
||||||
ensureDefaultValue(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
|
ensureDefaultValue(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
|
||||||
|
|
||||||
|
ensureDefaultBoolValue(AppConfig.PREF_LOCAL_DNS_ENABLED, true)
|
||||||
|
ensureDefaultBoolValue(AppConfig.PREF_FAKE_DNS_ENABLED, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureDefaultValue(key: String, default: String) {
|
private fun ensureDefaultValue(key: String, default: String) {
|
||||||
@@ -461,6 +463,12 @@ object SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ensureDefaultBoolValue(key: String, default: Boolean) {
|
||||||
|
if (MmkvManager.decodeSettingsBool(key) == null) {
|
||||||
|
MmkvManager.encodeSettings(key, default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun migrateAutoSort() {
|
private fun migrateAutoSort() {
|
||||||
val migrationKey = "auto_sort_migrated_v2"
|
val migrationKey = "auto_sort_migrated_v2"
|
||||||
if (MmkvManager.decodeSettingsBool(migrationKey, false)) {
|
if (MmkvManager.decodeSettingsBool(migrationKey, false)) {
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -592,12 +592,13 @@ object V2rayConfigManager {
|
|||||||
val hosts = mutableMapOf<String, Any>()
|
val hosts = mutableMapOf<String, Any>()
|
||||||
val servers = ArrayList<Any>()
|
val servers = ArrayList<Any>()
|
||||||
|
|
||||||
//remote Dns
|
|
||||||
val remoteDns = SettingsManager.getRemoteDnsServers()
|
val remoteDns = SettingsManager.getRemoteDnsServers()
|
||||||
|
val domesticDns = SettingsManager.getDomesticDnsServers()
|
||||||
val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY)
|
val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY)
|
||||||
remoteDns.forEach {
|
val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT)
|
||||||
servers.add(it)
|
val isCnRoutingMode = directDomain.contains(AppConfig.GEOSITE_CN)
|
||||||
}
|
val geoipCn = arrayListOf(AppConfig.GEOIP_CN)
|
||||||
|
|
||||||
if (proxyDomain.isNotEmpty()) {
|
if (proxyDomain.isNotEmpty()) {
|
||||||
servers.add(
|
servers.add(
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
@@ -607,11 +608,6 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// domestic DNS
|
|
||||||
val domesticDns = SettingsManager.getDomesticDnsServers()
|
|
||||||
val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT)
|
|
||||||
val isCnRoutingMode = directDomain.contains(AppConfig.GEOSITE_CN)
|
|
||||||
val geoipCn = arrayListOf(AppConfig.GEOIP_CN)
|
|
||||||
if (directDomain.isNotEmpty()) {
|
if (directDomain.isNotEmpty()) {
|
||||||
servers.add(
|
servers.add(
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
@@ -624,16 +620,17 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
//block dns
|
remoteDns.forEach {
|
||||||
|
servers.add(it)
|
||||||
|
}
|
||||||
|
|
||||||
val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED)
|
val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED)
|
||||||
if (blkDomain.isNotEmpty()) {
|
if (blkDomain.isNotEmpty()) {
|
||||||
hosts.putAll(blkDomain.map { it to AppConfig.LOOPBACK })
|
hosts.putAll(blkDomain.map { it to AppConfig.LOOPBACK })
|
||||||
}
|
}
|
||||||
|
|
||||||
// hardcode googleapi rule to fix play store problems
|
|
||||||
hosts[AppConfig.GOOGLEAPIS_CN_DOMAIN] = AppConfig.GOOGLEAPIS_COM_DOMAIN
|
hosts[AppConfig.GOOGLEAPIS_CN_DOMAIN] = AppConfig.GOOGLEAPIS_COM_DOMAIN
|
||||||
|
|
||||||
// hardcode popular Android Private DNS rule to fix localhost DNS problem
|
|
||||||
hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES
|
hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES
|
||||||
hosts[AppConfig.DNS_CLOUDFLARE_ONE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ONE_ADDRESSES
|
hosts[AppConfig.DNS_CLOUDFLARE_ONE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ONE_ADDRESSES
|
||||||
hosts[AppConfig.DNS_CLOUDFLARE_DNS_COM_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_COM_ADDRESSES
|
hosts[AppConfig.DNS_CLOUDFLARE_DNS_COM_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_COM_ADDRESSES
|
||||||
@@ -643,7 +640,6 @@ object V2rayConfigManager {
|
|||||||
hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES
|
hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES
|
||||||
hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES
|
hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES
|
||||||
|
|
||||||
//User DNS hosts
|
|
||||||
try {
|
try {
|
||||||
val userHosts = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
|
val userHosts = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
|
||||||
if (userHosts.isNotNullEmpty()) {
|
if (userHosts.isNotNullEmpty()) {
|
||||||
@@ -657,14 +653,12 @@ object V2rayConfigManager {
|
|||||||
Log.e(AppConfig.TAG, "Failed to configure user DNS hosts", e)
|
Log.e(AppConfig.TAG, "Failed to configure user DNS hosts", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNS dns
|
|
||||||
v2rayConfig.dns = V2rayConfig.DnsBean(
|
v2rayConfig.dns = V2rayConfig.DnsBean(
|
||||||
servers = servers,
|
servers = servers,
|
||||||
hosts = hosts,
|
hosts = hosts,
|
||||||
tag = AppConfig.TAG_DNS
|
tag = AppConfig.TAG_DNS
|
||||||
)
|
)
|
||||||
|
|
||||||
// DNS routing
|
|
||||||
v2rayConfig.routing.rules.add(
|
v2rayConfig.routing.rules.add(
|
||||||
RulesBean(
|
RulesBean(
|
||||||
outboundTag = AppConfig.TAG_DIRECT,
|
outboundTag = AppConfig.TAG_DIRECT,
|
||||||
@@ -1317,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import kotlinx.coroutines.SupervisorJob
|
|||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.joinAll
|
import kotlinx.coroutines.joinAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
@@ -75,13 +76,51 @@ class RealPingWorkerService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startRealPing(guid: String): Long {
|
private suspend fun startRealPing(guid: String): Long {
|
||||||
val retFailure = -1L
|
val retFailure = -1L
|
||||||
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||||
if (!configResult.status) {
|
if (!configResult.status) {
|
||||||
return retFailure
|
return retFailure
|
||||||
}
|
}
|
||||||
return V2RayNativeManager.measureOutboundDelay(configResult.content, SettingsManager.getDelayTestUrl())
|
|
||||||
|
var bestDelay = retFailure
|
||||||
|
|
||||||
|
for (attempt in 0 until 2) {
|
||||||
|
try {
|
||||||
|
val delay = withTimeout(10000L) {
|
||||||
|
V2RayNativeManager.measureOutboundDelay(
|
||||||
|
configResult.content,
|
||||||
|
SettingsManager.getDelayTestUrl()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delay > 0 && (bestDelay == retFailure || delay < bestDelay)) {
|
||||||
|
bestDelay = delay
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestDelay > 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (attempt == 0) {
|
||||||
|
try {
|
||||||
|
val delay = withTimeout(10000L) {
|
||||||
|
V2RayNativeManager.measureOutboundDelay(
|
||||||
|
configResult.content,
|
||||||
|
SettingsManager.getDelayTestUrl(true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delay > 0 && (bestDelay == retFailure || delay < bestDelay)) {
|
||||||
|
bestDelay = delay
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestDelay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -228,13 +230,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure DNS servers
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||||
//if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
builder.addDnsServer(vpnConfig.ipv4Router)
|
||||||
// builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
} else {
|
||||||
//} else {
|
SettingsManager.getVpnDnsServers().forEach {
|
||||||
SettingsManager.getVpnDnsServers().forEach {
|
if (Utils.isPureIpAddress(it)) {
|
||||||
if (Utils.isPureIpAddress(it)) {
|
builder.addDnsServer(it)
|
||||||
builder.addDnsServer(it)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -417,4 +417,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">جارٍ التحديث…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -423,4 +423,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">আপডেট হচ্ছে…</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -433,4 +433,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">در حال بهروزرسانی…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -432,4 +432,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">در حال بهروزرسانی…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -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">#90CAF9</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">#90CAF9</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">#64B5F6</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">#BBDEFB</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>
|
||||||
|
|||||||
@@ -419,4 +419,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">Đang cập nhật…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -425,4 +425,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">更新中…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -425,4 +425,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">更新中…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -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">#1565C0</color>
|
||||||
<color name="colorPingRed">#FF0099</color>
|
<color name="colorPingRed">#FF0099</color>
|
||||||
<color name="colorConfigType">#88AC8A</color>
|
<color name="colorConfigType">#1976D2</color>
|
||||||
<color name="colorWhite">#FFFFFF</color>
|
<color name="colorWhite">#FFFFFF</color>
|
||||||
<color name="color_fab_active">#88AC8A</color>
|
<color name="color_fab_active">#1976D2</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">#1976D2</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">#1565C0</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">#BBDEFB</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,12 +2,4 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.android.library) apply false
|
alias(libs.plugins.android.library) apply false
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
|
||||||
dependencies {
|
|
||||||
classpath(libs.gradle.license.plugin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,20 +15,7 @@ org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
|
|||||||
# Android operating system, and which are packaged with your app's APK
|
# Android operating system, and which are packaged with your app's APK
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
|
||||||
# thereby reducing the size of the R class for that library
|
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
kotlin.incremental=true
|
kotlin.incremental=true
|
||||||
android.defaults.buildfeatures.resvalues=true
|
android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false
|
||||||
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
|
|
||||||
android.enableAppCompileTimeRClass=false
|
|
||||||
android.usesSdkInManifest.disallowed=false
|
|
||||||
android.uniquePackageNames=false
|
|
||||||
android.dependency.useConstraints=true
|
|
||||||
android.r8.strictFullModeForKeepRules=false
|
|
||||||
android.r8.optimizedResourceShrinking=false
|
|
||||||
android.builtInKotlin=false
|
|
||||||
android.newDsl=false
|
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -136,9 +136,17 @@ fun main() {
|
|||||||
)
|
)
|
||||||
println("Добавлена подписка БЕЛЫЕ W: $guid2")
|
println("Добавлена подписка БЕЛЫЕ W: $guid2")
|
||||||
|
|
||||||
|
val guid3 = manager.addSubscription(
|
||||||
|
remarks = "KEY",
|
||||||
|
url = "https://key.zarazaex.xyz/sub",
|
||||||
|
autoUpdate = true
|
||||||
|
)
|
||||||
|
println("Добавлена подписка KEY: $guid3")
|
||||||
|
|
||||||
println("\nОбновление подписок...")
|
println("\nОбновление подписок...")
|
||||||
manager.updateSubscription(guid1, "https://raw.githubusercontent.com/zieng2/wl/refs/heads/main/vless_universal.txt")
|
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(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")
|
println("\nПодписки успешно добавлены и обновлены в $mmkvPath")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user