mirror of
https://github.com/openlibrecommunity/olcng.git
synced 2026-07-03 14:05:17 +02:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d803b8ff9 | |||
| 81d979546c | |||
| 9362d1ef01 | |||
| ac9760aedd | |||
| 1178b87fb7 | |||
| 4fea86944f | |||
| ff2fea8c03 | |||
| 415f2230e6 | |||
| fb827c1fb1 | |||
| cb4e3aab54 | |||
| 732d7248af | |||
| 0c32ddc642 | |||
| 153c545400 | |||
| 9af6472f8b | |||
| 346be42e37 | |||
| dd9e076ab1 | |||
| 903b0d4414 | |||
| f58a4f8f6f | |||
| fdce3ea2c1 | |||
| f0d620676d | |||
| b92a6cdfac | |||
| 45da2479dd | |||
| 1c936b2b31 | |||
| dbe109eedb | |||
| 7705aded77 |
@@ -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.
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ object AppConfig {
|
|||||||
const val PREF_USE_HEV_TUNNEL = "pref_use_hev_tunnel_v2"
|
const val PREF_USE_HEV_TUNNEL = "pref_use_hev_tunnel_v2"
|
||||||
const val PREF_HEV_TUNNEL_LOGLEVEL = "pref_hev_tunnel_loglevel"
|
const val PREF_HEV_TUNNEL_LOGLEVEL = "pref_hev_tunnel_loglevel"
|
||||||
const val PREF_HEV_TUNNEL_RW_TIMEOUT = "pref_hev_tunnel_rw_timeout_v2"
|
const val PREF_HEV_TUNNEL_RW_TIMEOUT = "pref_hev_tunnel_rw_timeout_v2"
|
||||||
const val PREF_AUTO_REMOVE_INVALID_AFTER_TEST = "pref_auto_remove_invalid_after_test"
|
|
||||||
const val PREF_AUTO_SORT_AFTER_TEST = "pref_auto_sort_after_test"
|
const val PREF_AUTO_SORT_AFTER_TEST = "pref_auto_sort_after_test"
|
||||||
|
|
||||||
/** Cache keys. */
|
/** Cache keys. */
|
||||||
|
|||||||
@@ -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,11 +225,11 @@ 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 {
|
||||||
|
return synchronized(getSubscriptionLock(subid)) {
|
||||||
try {
|
try {
|
||||||
if (servers == null) {
|
if (servers == null) {
|
||||||
return 0
|
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 }
|
||||||
@@ -233,7 +241,12 @@ object AngConfigManager {
|
|||||||
|
|
||||||
val subItem = MmkvManager.decodeSubscription(subid)
|
val subItem = MmkvManager.decodeSubscription(subid)
|
||||||
|
|
||||||
// Parse all configs first (no I/O during parsing)
|
val oldPingData = if (!append) {
|
||||||
|
saveOldPingData(subid)
|
||||||
|
} else {
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
val configs = mutableListOf<ProfileItem>()
|
val configs = mutableListOf<ProfileItem>()
|
||||||
servers.lines()
|
servers.lines()
|
||||||
.distinct()
|
.distinct()
|
||||||
@@ -245,21 +258,22 @@ object AngConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch save all parsed configs (only one serverList read/write)
|
|
||||||
if (configs.isNotEmpty()) {
|
if (configs.isNotEmpty()) {
|
||||||
if (!append) {
|
if (!append) {
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
}
|
}
|
||||||
val keyToProfile = batchSaveConfigs(configs, subid)
|
val keyToProfile = batchSaveConfigs(configs, subid, append)
|
||||||
|
restoreOldPingData(keyToProfile, oldPingData)
|
||||||
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
||||||
matchKey?.let { MmkvManager.setSelectServer(it) }
|
matchKey?.let { MmkvManager.setSelectServer(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return configs.size
|
return@synchronized configs.size
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse batch config", e)
|
Log.e(AppConfig.TAG, "Failed to parse batch config", e)
|
||||||
}
|
}
|
||||||
return 0
|
return@synchronized 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) {
|
||||||
|
serverList.mapNotNull { guid ->
|
||||||
MmkvManager.decodeServerConfig(guid)?.let { guid to it }
|
MmkvManager.decodeServerConfig(guid)?.let { guid to it }
|
||||||
}.toMap()
|
}.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,8 +422,9 @@ 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 {
|
||||||
|
return synchronized(getSubscriptionLock(subid)) {
|
||||||
if (server == null) {
|
if (server == null) {
|
||||||
return 0
|
return@synchronized 0
|
||||||
}
|
}
|
||||||
if (server.contains("inbounds")
|
if (server.contains("inbounds")
|
||||||
&& server.contains("outbounds")
|
&& server.contains("outbounds")
|
||||||
@@ -394,15 +447,14 @@ object AngConfigManager {
|
|||||||
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
||||||
count += 1
|
count += 1
|
||||||
}
|
}
|
||||||
return count
|
return@synchronized count
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
|
Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// For compatibility
|
val config = CustomFmt.parse(server) ?: return@synchronized 0
|
||||||
val config = CustomFmt.parse(server) ?: return 0
|
|
||||||
config.subscriptionId = subid
|
config.subscriptionId = subid
|
||||||
config.description = generateDescription(config)
|
config.description = generateDescription(config)
|
||||||
if (!append) {
|
if (!append) {
|
||||||
@@ -410,27 +462,28 @@ object AngConfigManager {
|
|||||||
}
|
}
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
MmkvManager.encodeServerRaw(key, server)
|
||||||
return 1
|
return@synchronized 1
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
|
Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
|
||||||
}
|
}
|
||||||
return 0
|
return@synchronized 0
|
||||||
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
||||||
try {
|
try {
|
||||||
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
|
val config = WireguardFmt.parseWireguardConfFile(server) ?: return@synchronized R.string.toast_incorrect_protocol
|
||||||
config.description = generateDescription(config)
|
config.description = generateDescription(config)
|
||||||
if (!append) {
|
if (!append) {
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
}
|
}
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
MmkvManager.encodeServerRaw(key, server)
|
||||||
return 1
|
return@synchronized 1
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
|
Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
|
||||||
}
|
}
|
||||||
return 0
|
return@synchronized 0
|
||||||
} else {
|
} else {
|
||||||
return 0
|
return@synchronized 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)
|
||||||
""
|
""
|
||||||
|
|||||||
@@ -292,34 +292,6 @@ object MmkvManager {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes invalid server configurations.
|
|
||||||
*
|
|
||||||
* @param guid The server GUID.
|
|
||||||
* @return The number of server configurations removed.
|
|
||||||
*/
|
|
||||||
fun removeInvalidServer(guid: String): Int {
|
|
||||||
var count = 0
|
|
||||||
if (guid.isNotEmpty()) {
|
|
||||||
decodeServerAffiliationInfo(guid)?.let { aff ->
|
|
||||||
if (aff.testDelayMillis < 0L) {
|
|
||||||
removeServer(guid)
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
serverAffStorage.allKeys()?.forEach { key ->
|
|
||||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
|
||||||
if (aff.testDelayMillis < 0L) {
|
|
||||||
removeServer(key)
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes the raw server configuration.
|
* Encodes the raw server configuration.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import java.io.FileOutputStream
|
|||||||
|
|
||||||
object UpdateCheckerManager {
|
object UpdateCheckerManager {
|
||||||
suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) {
|
suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
val url = if (includePreRelease) {
|
val url = if (includePreRelease) {
|
||||||
AppConfig.APP_API_URL
|
AppConfig.APP_API_URL
|
||||||
} else {
|
} else {
|
||||||
@@ -59,6 +60,13 @@ object UpdateCheckerManager {
|
|||||||
} else {
|
} else {
|
||||||
CheckUpdateResult(hasUpdate = false)
|
CheckUpdateResult(hasUpdate = false)
|
||||||
}
|
}
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to parse version: ${e.message}")
|
||||||
|
return@withContext CheckUpdateResult(hasUpdate = false)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to check for update: ${e.message}")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
|
suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
|
||||||
@@ -95,12 +103,15 @@ object UpdateCheckerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compareVersions(version1: String, version2: String): Int {
|
private fun compareVersions(version1: String, version2: String): Int {
|
||||||
val v1 = version1.split(".")
|
val cleanVersion1 = version1.split("-")[0]
|
||||||
val v2 = version2.split(".")
|
val cleanVersion2 = version2.split("-")[0]
|
||||||
|
|
||||||
|
val v1 = cleanVersion1.split(".")
|
||||||
|
val v2 = cleanVersion2.split(".")
|
||||||
|
|
||||||
for (i in 0 until maxOf(v1.size, v2.size)) {
|
for (i in 0 until maxOf(v1.size, v2.size)) {
|
||||||
val num1 = if (i < v1.size) v1[i].toInt() else 0
|
val num1 = if (i < v1.size) v1[i].toIntOrNull() ?: 0 else 0
|
||||||
val num2 = if (i < v2.size) v2[i].toInt() else 0
|
val num2 = if (i < v2.size) v2[i].toIntOrNull() ?: 0 else 0
|
||||||
if (num1 != num2) return num1 - num2
|
if (num1 != num2) return num1 - num2
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -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,6 +59,15 @@ 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) {
|
||||||
|
synchronized(operationLock) {
|
||||||
|
if (isOperationInProgress) {
|
||||||
|
Log.w(AppConfig.TAG, "StartCore-Manager: Operation already in progress")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isOperationInProgress = true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}")
|
Log.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}")
|
||||||
|
|
||||||
if (guid != null) {
|
if (guid != null) {
|
||||||
@@ -64,6 +75,11 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startContextService(context)
|
startContextService(context)
|
||||||
|
} finally {
|
||||||
|
synchronized(operationLock) {
|
||||||
|
isOperationInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,7 +87,7 @@ 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)
|
Log.i(AppConfig.TAG, "StartCore-Manager: stopVService called, serviceControl=${serviceControl?.get()}")
|
||||||
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
|
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,23 +244,26 @@ object V2RayServiceManager {
|
|||||||
fun stopCoreLoop(): Boolean {
|
fun stopCoreLoop(): Boolean {
|
||||||
val service = getService() ?: return false
|
val service = getService() ?: return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
service.unregisterReceiver(mMsgReceive)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to unregister receiver", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationManager.cancelNotification()
|
||||||
|
|
||||||
if (coreController.isRunning) {
|
if (coreController.isRunning) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
coreController.stopLoop()
|
coreController.stopLoop()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to stop V2Ray loop", e)
|
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to stop V2Ray loop", e)
|
||||||
}
|
} finally {
|
||||||
}
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
||||||
NotificationManager.cancelNotification()
|
|
||||||
|
|
||||||
try {
|
|
||||||
service.unregisterReceiver(mMsgReceive)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to unregister receiver", e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -365,34 +384,46 @@ object V2RayServiceManager {
|
|||||||
* @param intent The intent being received.
|
* @param intent The intent being received.
|
||||||
*/
|
*/
|
||||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||||
val serviceControl = serviceControl?.get() ?: return
|
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
when (intent?.getIntExtra("key", 0)) {
|
||||||
AppConfig.MSG_REGISTER_CLIENT -> {
|
AppConfig.MSG_REGISTER_CLIENT -> {
|
||||||
|
val svc = serviceControl?.get() ?: return
|
||||||
if (coreController.isRunning) {
|
if (coreController.isRunning) {
|
||||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
MessageUtil.sendMsg2UI(svc.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
||||||
} else {
|
} else {
|
||||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
MessageUtil.sendMsg2UI(svc.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_UNREGISTER_CLIENT -> {
|
AppConfig.MSG_UNREGISTER_CLIENT -> {
|
||||||
// nothing to do
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_START -> {
|
AppConfig.MSG_STATE_START -> {
|
||||||
// nothing to do
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_STOP -> {
|
AppConfig.MSG_STATE_STOP -> {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: Stop service")
|
Log.i(AppConfig.TAG, "StartCore-Manager: Stop service")
|
||||||
serviceControl.stopService()
|
synchronized(operationLock) {
|
||||||
|
isOperationInProgress = false
|
||||||
|
}
|
||||||
|
val svc = serviceControl?.get()
|
||||||
|
if (svc != null) {
|
||||||
|
svc.stopService()
|
||||||
|
} else if (ctx != null) {
|
||||||
|
Log.w(AppConfig.TAG, "StartCore-Manager: serviceControl null on stop, stopping core directly")
|
||||||
|
stopCoreLoop()
|
||||||
|
ctx.stopService(Intent(ctx, V2RayVpnService::class.java))
|
||||||
|
ctx.stopService(Intent(ctx, V2RayProxyOnlyService::class.java))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_RESTART -> {
|
AppConfig.MSG_STATE_RESTART -> {
|
||||||
Log.i(AppConfig.TAG, "StartCore-Manager: Restart service")
|
Log.i(AppConfig.TAG, "StartCore-Manager: Restart service")
|
||||||
serviceControl.stopService()
|
synchronized(operationLock) {
|
||||||
|
isOperationInProgress = false
|
||||||
|
}
|
||||||
|
serviceControl?.get()?.stopService()
|
||||||
Thread.sleep(500L)
|
Thread.sleep(500L)
|
||||||
startVService(serviceControl.getService())
|
if (ctx != null) startVService(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_DELAY -> {
|
AppConfig.MSG_MEASURE_DELAY -> {
|
||||||
|
|||||||
@@ -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,15 +620,9 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vpnDns.isNotEmpty()) {
|
|
||||||
vpnDns.forEach {
|
|
||||||
servers.add(it)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
remoteDns.forEach {
|
remoteDns.forEach {
|
||||||
servers.add(it)
|
servers.add(it)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED)
|
val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED)
|
||||||
if (blkDomain.isNotEmpty()) {
|
if (blkDomain.isNotEmpty()) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,13 +200,29 @@ 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)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
if (mainViewModel.isRunning.value == true) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
V2RayServiceManager.stopVService(this)
|
Log.d(AppConfig.TAG, "FAB: stopping service, isRunning=${mainViewModel.isRunning.value}")
|
||||||
|
V2RayServiceManager.stopVService(this@MainActivity)
|
||||||
} else {
|
} else {
|
||||||
|
Log.d(AppConfig.TAG, "FAB: starting service, isRunning=${mainViewModel.isRunning.value}")
|
||||||
startV2RayWithPermission()
|
startV2RayWithPermission()
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "FAB: error", e)
|
||||||
|
applyRunningState(isLoading = false, isRunning = mainViewModel.isRunning.value ?: false)
|
||||||
|
} finally {
|
||||||
|
isFabOperationInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLayoutTestClick() {
|
private fun handleLayoutTestClick() {
|
||||||
@@ -220,15 +235,23 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLiteAction() {
|
private fun handleLiteAction() {
|
||||||
|
if (isFabOperationInProgress) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isFabOperationInProgress = true
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
if (mainViewModel.isRunning.value == true) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
V2RayServiceManager.stopVService(this)
|
V2RayServiceManager.stopVService(this@MainActivity)
|
||||||
|
delay(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
showStatus("Обновление профилей...")
|
showStatus("Обновление профилей...")
|
||||||
showLoading()
|
showLoading()
|
||||||
isLiteTesting = true
|
isLiteTesting = true
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
val result = mainViewModel.updateConfigViaSubAll()
|
val result = mainViewModel.updateConfigViaSubAll()
|
||||||
delay(500L)
|
delay(500L)
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
@@ -245,6 +268,13 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
mainViewModel.testAllRealPing()
|
mainViewModel.testAllRealPing()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
delay(1500)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Error in handleLiteAction", e)
|
||||||
|
} finally {
|
||||||
|
isFabOperationInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startV2RayWithPermission() {
|
private fun startV2RayWithPermission() {
|
||||||
@@ -269,12 +299,24 @@ 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 {
|
||||||
|
if (mainViewModel.isRunning.value == true) {
|
||||||
|
V2RayServiceManager.stopVService(this@MainActivity)
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
startV2Ray()
|
startV2Ray()
|
||||||
|
delay(1000)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Error in restartV2Ray", e)
|
||||||
|
} finally {
|
||||||
|
isFabOperationInProgress = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,25 +565,6 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun delInvalidConfig() {
|
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
showLoading()
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val ret = mainViewModel.removeInvalidServer()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
showStatus(getString(R.string.title_del_config_count, ret))
|
|
||||||
hideLoading()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sortByTestResults() {
|
private fun sortByTestResults() {
|
||||||
showLoading()
|
showLoading()
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
return when (item.itemId) {
|
||||||
R.id.add_config -> {
|
R.id.add_config -> {
|
||||||
startActivity(Intent(this, SubEditActivity::class.java))
|
startActivity(Intent(this, SubEditActivity::class.java))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.sub_update -> {
|
R.id.sub_update -> {
|
||||||
showLoading()
|
if (viewModel.isUpdating.value == true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading()
|
||||||
|
viewModel.isUpdating.value = true
|
||||||
|
viewModel.subscriptionStatuses.value = emptyMap()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val result = AngConfigManager.updateConfigViaSubAll()
|
|
||||||
delay(500L)
|
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
if (result.successCount + result.failureCount + result.skipCount == 0) {
|
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 {
|
||||||
|
if (isEnabled) {
|
||||||
adapterListener?.onEdit(subId, position)
|
adapterListener?.onEdit(subId, position)
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
|
||||||
adapterListener?.onRemove(subId, position)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.itemSubSettingBinding.layoutRemove.isClickable = isEnabled
|
||||||
|
holder.itemSubSettingBinding.layoutRemove.alpha = if (isEnabled) 1.0f else 0.5f
|
||||||
|
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
||||||
|
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,11 +111,15 @@ 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 {
|
||||||
|
if (isEnabled) {
|
||||||
adapterListener?.onShare(subItem.url)
|
adapterListener?.onShare(subItem.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
||||||
return MainViewHolder(
|
return MainViewHolder(
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -442,23 +445,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes invalid servers.
|
|
||||||
* @return The number of removed servers.
|
|
||||||
*/
|
|
||||||
fun removeInvalidServer(): Int {
|
|
||||||
var count = 0
|
|
||||||
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
|
||||||
count += MmkvManager.removeInvalidServer("")
|
|
||||||
} else {
|
|
||||||
val serversCopy = serversCache.toList()
|
|
||||||
for (item in serversCopy) {
|
|
||||||
count += MmkvManager.removeInvalidServer(item.guid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sorts servers by their test results.
|
* Sorts servers by their test results.
|
||||||
*/
|
*/
|
||||||
@@ -577,10 +563,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
fun onTestsFinished() {
|
fun onTestsFinished() {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_REMOVE_INVALID_AFTER_TEST)) {
|
|
||||||
removeInvalidServer()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_SORT_AFTER_TEST, true)) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_SORT_AFTER_TEST, true)) {
|
||||||
sortByTestResults()
|
sortByTestResults()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -156,8 +156,6 @@
|
|||||||
<string name="title_pref_is_booted">Auto connect at startup</string>
|
<string name="title_pref_is_booted">Auto connect at startup</string>
|
||||||
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">Auto delete invalid config after testing</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">Test results may not be accurate; deleted config cannot be recovered.</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
||||||
|
|
||||||
@@ -417,4 +415,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">جارٍ التحديث…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -154,8 +154,6 @@
|
|||||||
<string name="title_pref_is_booted">Auto connect at startup</string>
|
<string name="title_pref_is_booted">Auto connect at startup</string>
|
||||||
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">Auto delete invalid config after testing</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">Test results may not be accurate; deleted config cannot be recovered.</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
||||||
|
|
||||||
@@ -423,4 +421,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">আপডেট হচ্ছে…</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -155,8 +155,6 @@
|
|||||||
<string name="title_pref_is_booted">منپیز خوتکار مجال ره ونی</string>
|
<string name="title_pref_is_booted">منپیز خوتکار مجال ره ونی</string>
|
||||||
<string name="summary_pref_is_booted">مجال ره وندن، خوساخوس و سرور پسند بیڌه منپیز ابۊ که گاشڌ نا مووفق بۊ.</string>
|
<string name="summary_pref_is_booted">مجال ره وندن، خوساخوس و سرور پسند بیڌه منپیز ابۊ که گاشڌ نا مووفق بۊ.</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">پاک کردن خوتکار کانفیگ نا موئتبر بئڌ آزمایش</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">نتیجه یل آزمایش گاشڌ دییق نبۊن؛ کانفیگ پاک وابیڌه ن نتری وورگنی.</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">ترتیب خوتکار بئڌ آزمایش</string>
|
<string name="title_pref_auto_sort_after_test">ترتیب خوتکار بئڌ آزمایش</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">نتیجه یل آزمایش گاشڌ دییق نبۊن؛</string>
|
<string name="summary_pref_auto_sort_after_test">نتیجه یل آزمایش گاشڌ دییق نبۊن؛</string>
|
||||||
|
|
||||||
@@ -433,4 +431,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">در حال بهروزرسانی…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -152,8 +152,6 @@
|
|||||||
<string name="title_pref_is_booted">اتصال خودکار هنگام راه اندازی</string>
|
<string name="title_pref_is_booted">اتصال خودکار هنگام راه اندازی</string>
|
||||||
<string name="summary_pref_is_booted">هنگام راه اندازی به طور خودکار به سرور انتخابی متصل می شود که ممکن است ناموفق باشد.</string>
|
<string name="summary_pref_is_booted">هنگام راه اندازی به طور خودکار به سرور انتخابی متصل می شود که ممکن است ناموفق باشد.</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">Auto delete invalid config after testing</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">Test results may not be accurate; deleted config cannot be recovered.</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
||||||
|
|
||||||
@@ -432,4 +430,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>
|
||||||
|
|||||||
@@ -156,8 +156,6 @@
|
|||||||
<string name="title_pref_is_booted">Автоподключение при запуске</string>
|
<string name="title_pref_is_booted">Автоподключение при запуске</string>
|
||||||
<string name="summary_pref_is_booted">Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным)</string>
|
<string name="summary_pref_is_booted">Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным)</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">Автоудаление нерабочих профилей</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">Автоматическое удаление нерабочих профилей после проверки (результаты проверки могут быть неточными; восстановить удалённые профили невозможно)</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">Автосортировка профилей</string>
|
<string name="title_pref_auto_sort_after_test">Автосортировка профилей</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">Автоматическая сортировка профилей после проверки (результаты проверки могут быть неточными)</string>
|
<string name="summary_pref_auto_sort_after_test">Автоматическая сортировка профилей после проверки (результаты проверки могут быть неточными)</string>
|
||||||
|
|
||||||
@@ -298,6 +296,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>
|
||||||
|
|||||||
@@ -153,8 +153,6 @@
|
|||||||
<string name="title_pref_is_booted">Auto connect at startup</string>
|
<string name="title_pref_is_booted">Auto connect at startup</string>
|
||||||
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">Auto delete invalid config after testing</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">Test results may not be accurate; deleted config cannot be recovered.</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
||||||
|
|
||||||
@@ -419,4 +417,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">Đang cập nhật…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -152,8 +152,6 @@
|
|||||||
<string name="title_pref_is_booted">开机时自动连接</string>
|
<string name="title_pref_is_booted">开机时自动连接</string>
|
||||||
<string name="summary_pref_is_booted">开机时自动连接选择的服务器,可能会不成功</string>
|
<string name="summary_pref_is_booted">开机时自动连接选择的服务器,可能会不成功</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">测试后自动删除无效配置</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">测试结果可能不准确;已删除的配置无法恢复。</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">测试后自动排序</string>
|
<string name="title_pref_auto_sort_after_test">测试后自动排序</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">测试结果可能不准确;</string>
|
<string name="summary_pref_auto_sort_after_test">测试结果可能不准确;</string>
|
||||||
|
|
||||||
@@ -425,4 +423,5 @@
|
|||||||
<item>WebDAV</item>
|
<item>WebDAV</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="title_updating">更新中…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -153,8 +153,6 @@
|
|||||||
<string name="title_pref_is_booted">開機時自動連線</string>
|
<string name="title_pref_is_booted">開機時自動連線</string>
|
||||||
<string name="summary_pref_is_booted">開機時自動連線選擇的伺服器,可能會不成功</string>
|
<string name="summary_pref_is_booted">開機時自動連線選擇的伺服器,可能會不成功</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">測試後自動刪除無效配置</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">測試結果可能不準確;已刪除的配置無法復原。 </string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">測試後自動排序</string>
|
<string name="title_pref_auto_sort_after_test">測試後自動排序</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">測試結果可能不準確;</string>
|
<string name="summary_pref_auto_sort_after_test">測試結果可能不準確;</string>
|
||||||
|
|
||||||
@@ -425,4 +423,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>
|
||||||
@@ -157,8 +157,6 @@
|
|||||||
<string name="title_pref_is_booted">Auto connect at startup</string>
|
<string name="title_pref_is_booted">Auto connect at startup</string>
|
||||||
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_remove_invalid_after_test">Auto delete invalid config after testing</string>
|
|
||||||
<string name="summary_pref_auto_remove_invalid_after_test">Test results may not be accurate; deleted config cannot be recovered.</string>
|
|
||||||
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
||||||
|
|
||||||
@@ -249,7 +247,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 +302,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>
|
||||||
|
|||||||
@@ -266,12 +266,6 @@
|
|||||||
android:summary="@string/summary_pref_is_booted"
|
android:summary="@string/summary_pref_is_booted"
|
||||||
android:title="@string/title_pref_is_booted" />
|
android:title="@string/title_pref_is_booted" />
|
||||||
|
|
||||||
<CheckBoxPreference
|
|
||||||
android:key="pref_auto_remove_invalid_after_test"
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:summary="@string/summary_pref_auto_remove_invalid_after_test"
|
|
||||||
android:title="@string/title_pref_auto_remove_invalid_after_test" />
|
|
||||||
|
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
android:key="pref_auto_sort_after_test"
|
android:key="pref_auto_sort_after_test"
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user