mirror of
https://github.com/openlibrecommunity/olcng.git
synced 2026-07-03 14:05:17 +02:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bf125b4f5 | |||
| 50297d88cc | |||
| 2975b0cd1a | |||
| ea1f584422 | |||
| 73c81d02e7 | |||
| 8d803b8ff9 | |||
| 81d979546c | |||
| 9362d1ef01 | |||
| ac9760aedd | |||
| 1178b87fb7 | |||
| 4fea86944f | |||
| ff2fea8c03 | |||
| 415f2230e6 | |||
| fb827c1fb1 | |||
| cb4e3aab54 | |||
| 732d7248af | |||
| 0c32ddc642 | |||
| 153c545400 | |||
| 9af6472f8b | |||
| 346be42e37 | |||
| dd9e076ab1 | |||
| 903b0d4414 | |||
| f58a4f8f6f | |||
| fdce3ea2c1 | |||
| f0d620676d |
@@ -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
|
||||||
@@ -168,5 +167,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
files: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*.apk
|
files: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*.apk
|
||||||
tag_name: ${{ github.event.inputs.release_tag || github.ref_name }}
|
tag_name: ${{ github.event.inputs.release_tag || github.ref_name }}
|
||||||
prerelease: true
|
prerelease: false
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
|
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package xyz.zarazaex.olc.dto
|
|||||||
|
|
||||||
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
|
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
|
||||||
fun getTestDelayString(): String {
|
fun getTestDelayString(): String {
|
||||||
if (testDelayMillis == 0L) {
|
return when {
|
||||||
return ""
|
testDelayMillis == 0L -> ""
|
||||||
}
|
testDelayMillis < 0L -> "Error"
|
||||||
return testDelayMillis.toString() + "ms"
|
else -> "${testDelayMillis}ms"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isReachable(): Boolean = testDelayMillis > 0L
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -334,10 +353,20 @@ object V2RayServiceManager {
|
|||||||
override fun shutdown(): Long {
|
override fun shutdown(): Long {
|
||||||
val serviceControl = serviceControl?.get() ?: return -1
|
val serviceControl = serviceControl?.get() ?: return -1
|
||||||
return try {
|
return try {
|
||||||
serviceControl.stopService()
|
Log.w(AppConfig.TAG, "StartCore-Manager: Core shutdown callback, attempting restart")
|
||||||
|
val service = serviceControl.getService()
|
||||||
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_NOT_RUNNING, "")
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
kotlinx.coroutines.delay(1000L)
|
||||||
|
val ctx = service.applicationContext
|
||||||
|
if (coreController.isRunning == false) {
|
||||||
|
Log.i(AppConfig.TAG, "StartCore-Manager: Restarting service after core shutdown")
|
||||||
|
startVService(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
0
|
0
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to stop service", e)
|
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to handle core shutdown", e)
|
||||||
-1
|
-1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,34 +394,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 -> {
|
||||||
|
|||||||
@@ -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,26 @@ class RealPingWorkerService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startRealPing(guid: String): Long {
|
private suspend fun startRealPing(guid: String): Long {
|
||||||
val retFailure = -1L
|
|
||||||
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||||
if (!configResult.status) {
|
if (!configResult.status) return -1L
|
||||||
return retFailure
|
|
||||||
|
val urls = listOf(
|
||||||
|
SettingsManager.getDelayTestUrl(),
|
||||||
|
SettingsManager.getDelayTestUrl(true)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (url in urls) {
|
||||||
|
try {
|
||||||
|
val delay = withTimeout(10000L) {
|
||||||
|
V2RayNativeManager.measureOutboundDelay(configResult.content, url)
|
||||||
}
|
}
|
||||||
return V2RayNativeManager.measureOutboundDelay(configResult.content, SettingsManager.getDelayTestUrl())
|
if (delay > 0) return delay
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import android.net.ProxyInfo
|
|||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.os.PowerManager
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
@@ -25,6 +26,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
|
||||||
@@ -34,6 +36,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
private lateinit var mInterface: ParcelFileDescriptor
|
private lateinit var mInterface: ParcelFileDescriptor
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
private var tun2SocksService: Tun2SocksControl? = null
|
private var tun2SocksService: Tun2SocksControl? = null
|
||||||
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
|
|
||||||
/**destroy
|
/**destroy
|
||||||
* Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
* Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
||||||
@@ -78,6 +81,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
||||||
StrictMode.setThreadPolicy(policy)
|
StrictMode.setThreadPolicy(policy)
|
||||||
V2RayServiceManager.serviceControl = SoftReference(this)
|
V2RayServiceManager.serviceControl = SoftReference(this)
|
||||||
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||||
|
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "v2rayng:vpn")
|
||||||
|
.also { it.acquire() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRevoke() {
|
override fun onRevoke() {
|
||||||
@@ -93,7 +99,10 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
Log.i(AppConfig.TAG, "StartCore-VPN: Service destroyed")
|
Log.i(AppConfig.TAG, "StartCore-VPN: Service destroyed")
|
||||||
|
wakeLock?.let { if (it.isHeld) it.release() }
|
||||||
|
wakeLock = null
|
||||||
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 {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import xyz.zarazaex.olc.handler.SettingsChangeManager
|
|||||||
import xyz.zarazaex.olc.handler.SettingsManager
|
import xyz.zarazaex.olc.handler.SettingsManager
|
||||||
import xyz.zarazaex.olc.handler.UpdateCheckerManager
|
import xyz.zarazaex.olc.handler.UpdateCheckerManager
|
||||||
import xyz.zarazaex.olc.handler.V2RayServiceManager
|
import xyz.zarazaex.olc.handler.V2RayServiceManager
|
||||||
|
import xyz.zarazaex.olc.util.MessageUtil
|
||||||
import xyz.zarazaex.olc.util.Utils
|
import xyz.zarazaex.olc.util.Utils
|
||||||
import xyz.zarazaex.olc.viewmodel.MainViewModel
|
import xyz.zarazaex.olc.viewmodel.MainViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -42,9 +43,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 +51,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) {
|
||||||
@@ -165,13 +165,15 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
mainViewModel.sortByTestResults()
|
mainViewModel.sortByTestResults()
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
|
|
||||||
val firstServer = mainViewModel.serversCache.firstOrNull()
|
val firstReachable = mainViewModel.serversCache.firstOrNull { cache ->
|
||||||
if (firstServer != null) {
|
(MmkvManager.decodeServerAffiliationInfo(cache.guid)?.testDelayMillis ?: 0L) > 0L
|
||||||
MmkvManager.setSelectServer(firstServer.guid)
|
}
|
||||||
|
if (firstReachable != null) {
|
||||||
|
MmkvManager.setSelectServer(firstReachable.guid)
|
||||||
showStatus("Подключаемся к быстрейшему серверу")
|
showStatus("Подключаемся к быстрейшему серверу")
|
||||||
startV2RayWithPermission()
|
startV2RayWithPermission()
|
||||||
} else {
|
} else {
|
||||||
showStatus("Серверы не найдены!")
|
showStatus("Нет доступных серверов!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,13 +203,37 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFabAction() {
|
private fun handleFabAction() {
|
||||||
|
if (isFabOperationInProgress) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isFabOperationInProgress = true
|
||||||
|
|
||||||
|
val actuallyRunning = V2RayServiceManager.isRunning()
|
||||||
|
if (mainViewModel.isRunning.value != actuallyRunning) {
|
||||||
|
Log.w(AppConfig.TAG, "FAB: UI state mismatch, syncing: vm=${mainViewModel.isRunning.value}, actual=$actuallyRunning")
|
||||||
|
mainViewModel.isRunning.value = actuallyRunning
|
||||||
|
isFabOperationInProgress = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
applyRunningState(isLoading = true, isRunning = false)
|
applyRunningState(isLoading = true, isRunning = false)
|
||||||
|
|
||||||
if (mainViewModel.isRunning.value == true) {
|
lifecycleScope.launch {
|
||||||
V2RayServiceManager.stopVService(this)
|
try {
|
||||||
|
if (actuallyRunning) {
|
||||||
|
Log.d(AppConfig.TAG, "FAB: stopping service")
|
||||||
|
V2RayServiceManager.stopVService(this@MainActivity)
|
||||||
} else {
|
} else {
|
||||||
|
Log.d(AppConfig.TAG, "FAB: starting service")
|
||||||
startV2RayWithPermission()
|
startV2RayWithPermission()
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "FAB: error", e)
|
||||||
|
applyRunningState(isLoading = false, isRunning = V2RayServiceManager.isRunning())
|
||||||
|
} finally {
|
||||||
|
isFabOperationInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLayoutTestClick() {
|
private fun handleLayoutTestClick() {
|
||||||
@@ -220,15 +246,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 +279,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 +310,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +378,12 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
val actuallyRunning = V2RayServiceManager.isRunning()
|
||||||
|
if (mainViewModel.isRunning.value != actuallyRunning) {
|
||||||
|
Log.w(AppConfig.TAG, "onResume: syncing state vm=${mainViewModel.isRunning.value}, actual=$actuallyRunning")
|
||||||
|
mainViewModel.isRunning.value = actuallyRunning
|
||||||
|
}
|
||||||
|
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -523,25 +582,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(
|
||||||
|
|||||||
@@ -96,7 +96,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val subServers = MmkvManager.decodeServerList(sub.guid)
|
val subServers = MmkvManager.decodeServerList(sub.guid)
|
||||||
subServers.forEach { guid ->
|
subServers.forEach { guid ->
|
||||||
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
|
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
|
||||||
allServers.add(ServerWithDelay(guid, if (delay <= 0L) 999999 else delay))
|
val sortKey = when {
|
||||||
|
delay > 0L -> delay
|
||||||
|
delay == 0L -> Long.MAX_VALUE - 1
|
||||||
|
else -> Long.MAX_VALUE
|
||||||
|
}
|
||||||
|
allServers.add(ServerWithDelay(guid, sortKey))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,6 +271,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 +450,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.
|
||||||
*/
|
*/
|
||||||
@@ -496,7 +487,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val serverList = MmkvManager.decodeServerList(sub.guid)
|
val serverList = MmkvManager.decodeServerList(sub.guid)
|
||||||
serverList.forEach { guid ->
|
serverList.forEach { guid ->
|
||||||
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
|
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
|
||||||
allServerDelays.add(ServerDelay(guid, if (delay <= 0L) 999999 else delay, sub.guid))
|
val sortKey = when {
|
||||||
|
delay > 0L -> delay
|
||||||
|
delay == 0L -> Long.MAX_VALUE - 1
|
||||||
|
else -> Long.MAX_VALUE
|
||||||
|
}
|
||||||
|
allServerDelays.add(ServerDelay(guid, sortKey, sub.guid))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,7 +517,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
serverListToSort.forEach { key ->
|
serverListToSort.forEach { key ->
|
||||||
val delay = MmkvManager.decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L
|
val delay = MmkvManager.decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L
|
||||||
serverDelays.add(ServerDelay(key, if (delay <= 0L) 999999 else delay))
|
val sortKey = when {
|
||||||
|
delay > 0L -> delay
|
||||||
|
delay == 0L -> Long.MAX_VALUE - 1
|
||||||
|
else -> Long.MAX_VALUE
|
||||||
|
}
|
||||||
|
serverDelays.add(ServerDelay(key, sortKey))
|
||||||
}
|
}
|
||||||
serverDelays.sortBy { it.testDelayMillis }
|
serverDelays.sortBy { it.testDelayMillis }
|
||||||
|
|
||||||
@@ -577,10 +578,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,10 +168,27 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_last_updated"
|
android:id="@+id/tv_last_updated"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Caption"/>
|
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_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/padding_spacing_dp8"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</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