Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa4432584e | |||
| bb1c3d915f | |||
| d8983a4761 | |||
| 9815041c77 | |||
| 41b0983582 | |||
| bb1f677d45 | |||
| de1c8bcfcc | |||
| ca6e963575 | |||
| bfb6813dc7 | |||
| cc2b4eec65 | |||
| dd11569969 | |||
| 11c3dd97c3 | |||
| 92f15150cd | |||
| f8e809da8b | |||
| 12b9349e62 | |||
| a580494bde | |||
| 66f100ebb3 | |||
| b0213ffdf6 | |||
| 538b2eb0f8 | |||
| f2093c4c52 | |||
| 02ac57a4c6 | |||
| 379266f205 | |||
| 23dbb35da7 |
@@ -25,8 +25,8 @@ jobs:
|
|||||||
uses: android-actions/setup-android@v3.2.0
|
uses: android-actions/setup-android@v3.2.0
|
||||||
with:
|
with:
|
||||||
log-accepted-android-sdk-licenses: false
|
log-accepted-android-sdk-licenses: false
|
||||||
cmdline-tools-version: '12266719'
|
cmdline-tools-version: '13114758'
|
||||||
packages: 'platforms;android-35 build-tools;35.0.0 platform-tools'
|
packages: 'platforms;android-36.1 build-tools;36.1.0 platform-tools'
|
||||||
|
|
||||||
- name: Install NDK
|
- name: Install NDK
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+1
-1
Submodule AndroidLibXrayLite updated: 16222c3387...df33063839
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 701
|
versionCode = 705
|
||||||
versionName = "2.0.1"
|
versionName = "2.0.5"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
|
|||||||
@@ -42,6 +42,13 @@
|
|||||||
"name": "xray0",
|
"name": "xray0",
|
||||||
"MTU": 1500,
|
"MTU": 1500,
|
||||||
"userLevel": 8
|
"userLevel": 8
|
||||||
|
},
|
||||||
|
"sniffing": {
|
||||||
|
"enabled": true,
|
||||||
|
"destOverride": [
|
||||||
|
"http",
|
||||||
|
"tls"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,6 +4,5 @@ data class ConfigResult(
|
|||||||
var status: Boolean,
|
var status: Boolean,
|
||||||
var guid: String? = null,
|
var guid: String? = null,
|
||||||
var content: String = "",
|
var content: String = "",
|
||||||
var socksPort: Int? = null,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ data class V2rayConfig(
|
|||||||
var protocol: String,
|
var protocol: String,
|
||||||
var listen: String? = null,
|
var listen: String? = null,
|
||||||
var settings: InSettingsBean? = null,
|
var settings: InSettingsBean? = null,
|
||||||
val sniffing: SniffingBean? = null,
|
var sniffing: SniffingBean? = null,
|
||||||
val streamSettings: Any? = null,
|
val streamSettings: Any? = null,
|
||||||
val allocate: Any? = null
|
val allocate: Any? = null
|
||||||
) {
|
) {
|
||||||
@@ -88,6 +88,7 @@ data class V2rayConfig(
|
|||||||
var reserved: List<Int>? = null,
|
var reserved: List<Int>? = null,
|
||||||
var mtu: Int? = null,
|
var mtu: Int? = null,
|
||||||
var obfsPassword: String? = null,
|
var obfsPassword: String? = null,
|
||||||
|
var version: Int? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class VnextBean(
|
data class VnextBean(
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ inline fun <reified T : Serializable> Intent.serializable(key: String): T? = whe
|
|||||||
*
|
*
|
||||||
* @return True if the CharSequence is not null and not empty, false otherwise.
|
* @return True if the CharSequence is not null and not empty, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun CharSequence?.isNotNullEmpty(): Boolean = this != null && this.isNotEmpty()
|
fun CharSequence?.isNotNullEmpty(): Boolean = !this.isNullOrBlank()
|
||||||
|
|
||||||
fun String.concatUrl(vararg paths: String): String {
|
fun String.concatUrl(vararg paths: String): String {
|
||||||
val builder = StringBuilder(this.trimEnd('/'))
|
val builder = StringBuilder(this.trimEnd('/'))
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
@@ -96,6 +96,7 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
outboundBean.settings?.let { server ->
|
outboundBean.settings?.let { server ->
|
||||||
server.address = getServerAddress(profileItem)
|
server.address = getServerAddress(profileItem)
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
server.version = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
val sni = outboundBean.streamSettings?.let {
|
val sni = outboundBean.streamSettings?.let {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ object ShadowsocksFmt : FmtBase() {
|
|||||||
if (uri.port <= 0) return null
|
if (uri.port <= 0) return null
|
||||||
if (uri.userInfo.isNullOrEmpty()) return null
|
if (uri.userInfo.isNullOrEmpty()) return null
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ object SocksFmt : FmtBase() {
|
|||||||
if (uri.idnHost.isEmpty()) return null
|
if (uri.idnHost.isEmpty()) return null
|
||||||
if (uri.port <= 0) return null
|
if (uri.port <= 0) return null
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ object TrojanFmt : FmtBase() {
|
|||||||
val config = ProfileItem.create(EConfigType.TROJAN)
|
val config = ProfileItem.create(EConfigType.TROJAN)
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ object VlessFmt : FmtBase() {
|
|||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
val queryParam = getQueryParam(uri)
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ object VmessFmt : FmtBase() {
|
|||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
val queryParam = getQueryParam(uri)
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ object WireguardFmt : FmtBase() {
|
|||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
val queryParam = getQueryParam(uri)
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|||||||
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
|
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
|
||||||
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||||
|
import com.v2ray.ang.AppConfig.VPN
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.Language
|
import com.v2ray.ang.dto.Language
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
@@ -386,6 +387,15 @@ object SettingsManager {
|
|||||||
return MmkvManager.decodeSettingsBool(AppConfig.PREF_USE_HEV_TUNNEL, true)
|
return MmkvManager.decodeSettingsBool(AppConfig.PREF_USE_HEV_TUNNEL, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if VPN mode is enabled.
|
||||||
|
* @return True if VPN mode is enabled, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isVpnMode(): Boolean {
|
||||||
|
val mode = MmkvManager.decodeSettingsString(AppConfig.PREF_MODE)
|
||||||
|
return mode == null || mode == VPN
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure default settings are present in MMKV.
|
* Ensure default settings are present in MMKV.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ object V2RayServiceManager {
|
|||||||
} else {
|
} else {
|
||||||
context.toast(R.string.toast_services_start)
|
context.toast(R.string.toast_services_start)
|
||||||
}
|
}
|
||||||
val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: AppConfig.VPN) == AppConfig.VPN) {
|
val intent = if (SettingsManager.isVpnMode()) {
|
||||||
Intent(context.applicationContext, V2RayVpnService::class.java)
|
Intent(context.applicationContext, V2RayVpnService::class.java)
|
||||||
} else {
|
} else {
|
||||||
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.v2ray.ang.handler
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.google.gson.JsonArray
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.ConfigResult
|
import com.v2ray.ang.dto.ConfigResult
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
@@ -44,7 +45,7 @@ object V2rayConfigManager {
|
|||||||
try {
|
try {
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
|
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
|
||||||
return if (config.configType == EConfigType.CUSTOM) {
|
return if (config.configType == EConfigType.CUSTOM) {
|
||||||
getV2rayCustomConfig(guid, config)
|
getV2rayCustomConfig(context, guid, config)
|
||||||
} else if (config.configType == EConfigType.POLICYGROUP) {
|
} else if (config.configType == EConfigType.POLICYGROUP) {
|
||||||
getV2rayGroupConfig(context, guid, config)
|
getV2rayGroupConfig(context, guid, config)
|
||||||
} else {
|
} else {
|
||||||
@@ -67,7 +68,7 @@ object V2rayConfigManager {
|
|||||||
try {
|
try {
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
|
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
|
||||||
return if (config.configType == EConfigType.CUSTOM) {
|
return if (config.configType == EConfigType.CUSTOM) {
|
||||||
getV2rayCustomConfig(guid, config)
|
getV2rayCustomConfig(context, guid, config)
|
||||||
} else if (config.configType == EConfigType.POLICYGROUP) {
|
} else if (config.configType == EConfigType.POLICYGROUP) {
|
||||||
// The number of policy groups will not be very large, so no special handling is needed.
|
// The number of policy groups will not be very large, so no special handling is needed.
|
||||||
getV2rayGroupConfig(context, guid, config)
|
getV2rayGroupConfig(context, guid, config)
|
||||||
@@ -87,9 +88,43 @@ object V2rayConfigManager {
|
|||||||
* @param config The profile item containing the configuration details.
|
* @param config The profile item containing the configuration details.
|
||||||
* @return A ConfigResult object containing the result of the configuration retrieval.
|
* @return A ConfigResult object containing the result of the configuration retrieval.
|
||||||
*/
|
*/
|
||||||
private fun getV2rayCustomConfig(guid: String, config: ProfileItem): ConfigResult {
|
private fun getV2rayCustomConfig(context: Context, guid: String, config: ProfileItem): ConfigResult {
|
||||||
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
|
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
|
||||||
return ConfigResult(true, guid, raw)
|
val result = ConfigResult(true, guid, raw)
|
||||||
|
if (!needTun()) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if tun inbound exists
|
||||||
|
val json = JsonUtil.parseString(raw) ?: return result
|
||||||
|
val inboundsJson = if (json.has("inbounds") && json.get("inbounds")?.isJsonNull == false) {
|
||||||
|
json.getAsJsonArray("inbounds")
|
||||||
|
} else {
|
||||||
|
JsonArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in 0 until inboundsJson.size()) {
|
||||||
|
val elem = inboundsJson.get(i)
|
||||||
|
if (elem.isJsonObject) {
|
||||||
|
val inb = elem.asJsonObject
|
||||||
|
val tag = if (inb.has("tag") && inb.get("tag")?.isJsonNull == false) inb.get("tag").asString else ""
|
||||||
|
if (tag == "tun") return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add tun inbound from template
|
||||||
|
val templateConfig = initV2rayConfig(context) ?: return result
|
||||||
|
val inboundTun = templateConfig.inbounds.firstOrNull { it.tag == "tun" } ?: return result
|
||||||
|
inboundTun.settings?.mtu = SettingsManager.getVpnMtu()
|
||||||
|
|
||||||
|
// add to json
|
||||||
|
inboundsJson.add(JsonUtil.parseString(JsonUtil.toJson(inboundTun)))
|
||||||
|
if (inboundsJson.size() == 1) {
|
||||||
|
json.add("inbounds", inboundsJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
val updatedRaw = JsonUtil.toJsonPretty(json) ?: return result
|
||||||
|
return ConfigResult(true, guid, updatedRaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -299,18 +334,18 @@ object V2rayConfigManager {
|
|||||||
*/
|
*/
|
||||||
private fun initV2rayConfig(context: Context): V2rayConfig? {
|
private fun initV2rayConfig(context: Context): V2rayConfig? {
|
||||||
var assets = ""
|
var assets = ""
|
||||||
if (SettingsManager.isUsingHevTun()) {
|
if (needTun()) {
|
||||||
assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json")
|
|
||||||
if (TextUtils.isEmpty(assets)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
initConfigCache = assets
|
|
||||||
} else {
|
|
||||||
assets = initConfigCacheWithTun ?: Utils.readTextFromAssets(context, "v2ray_config_with_tun.json")
|
assets = initConfigCacheWithTun ?: Utils.readTextFromAssets(context, "v2ray_config_with_tun.json")
|
||||||
if (TextUtils.isEmpty(assets)) {
|
if (TextUtils.isEmpty(assets)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
initConfigCacheWithTun = assets
|
initConfigCacheWithTun = assets
|
||||||
|
} else {
|
||||||
|
assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json")
|
||||||
|
if (TextUtils.isEmpty(assets)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
initConfigCache = assets
|
||||||
}
|
}
|
||||||
val config = JsonUtil.fromJson(assets, V2rayConfig::class.java)
|
val config = JsonUtil.fromJson(assets, V2rayConfig::class.java)
|
||||||
return config
|
return config
|
||||||
@@ -322,6 +357,10 @@ object V2rayConfigManager {
|
|||||||
|
|
||||||
//region some sub function
|
//region some sub function
|
||||||
|
|
||||||
|
private fun needTun(): Boolean {
|
||||||
|
return SettingsManager.isVpnMode() && !SettingsManager.isUsingHevTun()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the inbound settings for V2ray.
|
* Configures the inbound settings for V2ray.
|
||||||
*
|
*
|
||||||
@@ -360,9 +399,10 @@ object V2rayConfigManager {
|
|||||||
v2rayConfig.inbounds.add(inbound2)
|
v2rayConfig.inbounds.add(inbound2)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SettingsManager.isUsingHevTun()) {
|
if (needTun()) {
|
||||||
val inboundTun = v2rayConfig.inbounds.firstOrNull { e -> e.tag == "tun" }
|
val inboundTun = v2rayConfig.inbounds.firstOrNull { e -> e.tag == "tun" }
|
||||||
inboundTun?.settings?.mtu = SettingsManager.getVpnMtu()
|
inboundTun?.settings?.mtu = SettingsManager.getVpnMtu()
|
||||||
|
inboundTun?.sniffing = inbound1.sniffing
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to configure inbounds", e)
|
Log.e(AppConfig.TAG, "Failed to configure inbounds", e)
|
||||||
@@ -484,23 +524,25 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsManager.isUsingHevTun()) {
|
if(SettingsManager.isVpnMode()) {
|
||||||
//hev-socks5-tunnel dns routing
|
if (SettingsManager.isUsingHevTun()) {
|
||||||
v2rayConfig.routing.rules.add(
|
//hev-socks5-tunnel dns routing
|
||||||
0, RulesBean(
|
v2rayConfig.routing.rules.add(
|
||||||
inboundTag = arrayListOf("socks"),
|
0, RulesBean(
|
||||||
outboundTag = "dns-out",
|
inboundTag = arrayListOf("socks"),
|
||||||
port = "53",
|
outboundTag = "dns-out",
|
||||||
|
port = "53",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
} else {
|
||||||
} else {
|
v2rayConfig.routing.rules.add(
|
||||||
v2rayConfig.routing.rules.add(
|
0, RulesBean(
|
||||||
0, RulesBean(
|
inboundTag = arrayListOf("tun"),
|
||||||
inboundTag = arrayListOf("tun"),
|
outboundTag = "dns-out",
|
||||||
outboundTag = "dns-out",
|
port = "53",
|
||||||
port = "53",
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNS outbound
|
// DNS outbound
|
||||||
@@ -1192,7 +1234,11 @@ object V2rayConfigManager {
|
|||||||
if (profileItem.portHopping.isNotNullEmpty()) {
|
if (profileItem.portHopping.isNotNullEmpty()) {
|
||||||
hysteriaSetting.udphop = StreamSettingsBean.HysteriaSettingsBean.HysteriaUdpHopBean(
|
hysteriaSetting.udphop = StreamSettingsBean.HysteriaSettingsBean.HysteriaUdpHopBean(
|
||||||
port = profileItem.portHopping,
|
port = profileItem.portHopping,
|
||||||
interval = profileItem.portHoppingInterval?.ifEmpty { "30" }.orEmpty().toInt()
|
interval = profileItem.portHoppingInterval
|
||||||
|
?.trim()
|
||||||
|
?.toIntOrNull()
|
||||||
|
?.takeIf { it >= 5 }
|
||||||
|
?: 30
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
streamSettings.hysteriaSettings = hysteriaSetting
|
streamSettings.hysteriaSettings = hysteriaSetting
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.v2ray.ang.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.handler.V2RayNativeManager
|
||||||
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
|
import com.v2ray.ang.util.MessageUtil
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineName
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.joinAll
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Worker that runs a batch of real-ping tests independently.
|
||||||
|
* Each batch owns its own CoroutineScope/dispatcher and can be cancelled separately.
|
||||||
|
*/
|
||||||
|
class RealPingWorkerService(
|
||||||
|
private val context: Context,
|
||||||
|
private val guids: List<String>,
|
||||||
|
private val onFinish: (status: String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
private val job = SupervisorJob()
|
||||||
|
private val cpu = Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
|
||||||
|
private val dispatcher = Executors.newFixedThreadPool(cpu * 4).asCoroutineDispatcher()
|
||||||
|
private val scope = CoroutineScope(job + dispatcher + CoroutineName("RealPingBatchWorker"))
|
||||||
|
|
||||||
|
private val runningCount = AtomicInteger(0)
|
||||||
|
private val totalCount = AtomicInteger(0)
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
val jobs = guids.map { guid ->
|
||||||
|
totalCount.incrementAndGet()
|
||||||
|
scope.launch {
|
||||||
|
runningCount.incrementAndGet()
|
||||||
|
try {
|
||||||
|
val result = startRealPing(guid)
|
||||||
|
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result))
|
||||||
|
} finally {
|
||||||
|
val count = totalCount.decrementAndGet()
|
||||||
|
val left = runningCount.decrementAndGet()
|
||||||
|
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / $count")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
joinAll(*jobs.toTypedArray())
|
||||||
|
onFinish("0")
|
||||||
|
} catch (_: CancellationException) {
|
||||||
|
onFinish("-1")
|
||||||
|
} finally {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun close() {
|
||||||
|
try {
|
||||||
|
dispatcher.close()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startRealPing(guid: String): Long {
|
||||||
|
val retFailure = -1L
|
||||||
|
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||||
|
if (!configResult.status) {
|
||||||
|
return retFailure
|
||||||
|
}
|
||||||
|
return V2RayNativeManager.measureOutboundDelay(configResult.content, SettingsManager.getDelayTestUrl())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,34 +6,15 @@ import android.os.IBinder
|
|||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
|
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
|
||||||
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
|
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
|
||||||
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
|
|
||||||
import com.v2ray.ang.extension.serializable
|
import com.v2ray.ang.extension.serializable
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
|
||||||
import com.v2ray.ang.handler.V2RayNativeManager
|
import com.v2ray.ang.handler.V2RayNativeManager
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import kotlinx.coroutines.CancellationException
|
import java.util.Collections
|
||||||
import kotlinx.coroutines.CoroutineName
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.cancelChildren
|
|
||||||
import kotlinx.coroutines.joinAll
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
|
||||||
|
|
||||||
class V2RayTestService : Service() {
|
class V2RayTestService : Service() {
|
||||||
private val realTestJob = SupervisorJob()
|
|
||||||
private val cpu = Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
|
|
||||||
private val realDispatcher = Executors.newFixedThreadPool((cpu * 2)).asCoroutineDispatcher()
|
|
||||||
private val realTestScope = CoroutineScope(
|
|
||||||
realTestJob + realDispatcher + CoroutineName("RealTest")
|
|
||||||
)
|
|
||||||
|
|
||||||
// simple counter for currently running tasks
|
// manage active batch workers so each batch is independent and cancellable
|
||||||
private val realTestRunningCount = AtomicInteger(0)
|
private val activeWorkers = Collections.synchronizedList(mutableListOf<RealPingWorkerService>())
|
||||||
private val realTestCount = AtomicInteger(0)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the V2Ray environment.
|
* Initializes the V2Ray environment.
|
||||||
@@ -57,8 +38,10 @@ class V2RayTestService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
realTestJob.cancel()
|
// cancel any active workers
|
||||||
realDispatcher.close()
|
val snapshot = ArrayList(activeWorkers)
|
||||||
|
snapshot.forEach { it.cancel() }
|
||||||
|
activeWorkers.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,64 +56,24 @@ class V2RayTestService : Service() {
|
|||||||
MSG_MEASURE_CONFIG -> {
|
MSG_MEASURE_CONFIG -> {
|
||||||
val guidsList = intent.serializable<ArrayList<String>>("content")
|
val guidsList = intent.serializable<ArrayList<String>>("content")
|
||||||
if (guidsList != null && guidsList.isNotEmpty()) {
|
if (guidsList != null && guidsList.isNotEmpty()) {
|
||||||
startBatchRealPing(guidsList)
|
lateinit var worker: RealPingWorkerService
|
||||||
|
worker = RealPingWorkerService(this, guidsList) { status ->
|
||||||
|
// notify UI and remove the worker from active list when finished
|
||||||
|
MessageUtil.sendMsg2UI(this@V2RayTestService, AppConfig.MSG_MEASURE_CONFIG_FINISH, status)
|
||||||
|
activeWorkers.remove(worker)
|
||||||
|
}
|
||||||
|
activeWorkers.add(worker)
|
||||||
|
worker.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MSG_MEASURE_CONFIG_CANCEL -> {
|
MSG_MEASURE_CONFIG_CANCEL -> {
|
||||||
realTestJob.cancelChildren()
|
// cancel all running batch workers independently
|
||||||
|
val snapshot = ArrayList(activeWorkers)
|
||||||
|
snapshot.forEach { it.cancel() }
|
||||||
|
activeWorkers.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts batch real ping tests.
|
|
||||||
* @param guidsList The list of GUIDs to test.
|
|
||||||
*/
|
|
||||||
private fun startBatchRealPing(guidsList: List<String>) {
|
|
||||||
val jobs = guidsList.map { guid ->
|
|
||||||
realTestCount.incrementAndGet()
|
|
||||||
realTestScope.launch {
|
|
||||||
realTestRunningCount.incrementAndGet()
|
|
||||||
try {
|
|
||||||
val result = startRealPing(guid)
|
|
||||||
MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result))
|
|
||||||
} finally {
|
|
||||||
val count = realTestCount.decrementAndGet()
|
|
||||||
val left = realTestRunningCount.decrementAndGet()
|
|
||||||
MessageUtil.sendMsg2UI(this@V2RayTestService, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / $count")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
realTestScope.launch {
|
|
||||||
try {
|
|
||||||
joinAll(*jobs.toTypedArray())
|
|
||||||
notifyAllTasksCompleted("0")
|
|
||||||
} catch (_: CancellationException) {
|
|
||||||
notifyAllTasksCompleted("-1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun notifyAllTasksCompleted(status: String) {
|
|
||||||
MessageUtil.sendMsg2UI(this@V2RayTestService, AppConfig.MSG_MEASURE_CONFIG_FINISH, status)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the real ping test.
|
|
||||||
* @param guid The GUID of the configuration.
|
|
||||||
* @return The ping result.
|
|
||||||
*/
|
|
||||||
private fun startRealPing(guid: String): Long {
|
|
||||||
val retFailure = -1L
|
|
||||||
|
|
||||||
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(this, guid)
|
|
||||||
if (!configResult.status) {
|
|
||||||
return retFailure
|
|
||||||
}
|
|
||||||
return V2RayNativeManager.measureOutboundDelay(configResult.content, SettingsManager.getDelayTestUrl())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A common Adapter -> host callback interface that includes common actions: edit, remove and refresh.
|
||||||
|
* Extend this interface or define more specific interfaces for different adapters as needed.
|
||||||
|
*/
|
||||||
|
interface BaseAdapterListener {
|
||||||
|
/**
|
||||||
|
* Request the host to edit the specified item.
|
||||||
|
* @param guid Unique identifier (GUID) of the item
|
||||||
|
* @param position Current position in the adapter (optional; host should validate it)
|
||||||
|
*/
|
||||||
|
fun onEdit(guid: String, position: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the host to remove the specified item. Position is provided for optional animation or validation.
|
||||||
|
* @param guid Unique identifier (GUID) of the item
|
||||||
|
* @param position Current position in the adapter (optional; host should validate it)
|
||||||
|
*/
|
||||||
|
fun onRemove(guid: String, position: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the host to share the specified URL.
|
||||||
|
* @param url The URL to be shared
|
||||||
|
*/
|
||||||
|
fun onShare(url: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the host to refresh data (for example, reload from the ViewModel or call notifyDataSetChanged).
|
||||||
|
*/
|
||||||
|
fun onRefreshData()
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import com.v2ray.ang.dto.GroupMapItem
|
|||||||
class GroupPagerAdapter(activity: FragmentActivity, var groups: List<GroupMapItem>) : FragmentStateAdapter(activity) {
|
class GroupPagerAdapter(activity: FragmentActivity, var groups: List<GroupMapItem>) : FragmentStateAdapter(activity) {
|
||||||
override fun getItemCount(): Int = groups.size
|
override fun getItemCount(): Int = groups.size
|
||||||
override fun createFragment(position: Int) = GroupServerFragment.newInstance(groups[position].id)
|
override fun createFragment(position: Int) = GroupServerFragment.newInstance(groups[position].id)
|
||||||
|
override fun getItemId(position: Int): Long = groups[position].id.hashCode().toLong()
|
||||||
|
override fun containsItem(itemId: Long): Boolean = groups.any { it.id.hashCode().toLong() == itemId }
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun update(groups: List<GroupMapItem>) {
|
fun update(groups: List<GroupMapItem>) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -9,7 +8,6 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.TAG
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.FragmentGroupServerBinding
|
import com.v2ray.ang.databinding.FragmentGroupServerBinding
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
@@ -51,14 +49,14 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
|||||||
if (mainViewModel.subscriptionId != subId) {
|
if (mainViewModel.subscriptionId != subId) {
|
||||||
return@observe
|
return@observe
|
||||||
}
|
}
|
||||||
Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
|
// Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
|
||||||
adapter.setData(mainViewModel.serversCache, index)
|
adapter.setData(mainViewModel.serversCache, index)
|
||||||
}
|
}
|
||||||
mainViewModel.isRunning.observe(viewLifecycleOwner) { isRunning ->
|
mainViewModel.isRunning.observe(viewLifecycleOwner) { isRunning ->
|
||||||
adapter.isRunning = isRunning
|
adapter.isRunning = isRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
|
// Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|||||||
@@ -2,37 +2,34 @@ package com.v2ray.ang.ui
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
import com.v2ray.ang.viewmodel.LogcatViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
|
|
||||||
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||||
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
||||||
|
private val viewModel: LogcatViewModel by viewModels()
|
||||||
private var logsetsAll: MutableList<String> = mutableListOf()
|
private lateinit var adapter: LogcatRecyclerAdapter
|
||||||
var logsets: MutableList<String> = mutableListOf()
|
|
||||||
private val adapter by lazy { LogcatRecyclerAdapter(this) }
|
|
||||||
|
|
||||||
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_logcat))
|
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_logcat))
|
||||||
|
|
||||||
|
adapter = LogcatRecyclerAdapter(viewModel, ::onLogLongClick)
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
@@ -40,58 +37,12 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
|||||||
|
|
||||||
binding.refreshLayout.setOnRefreshListener(this)
|
binding.refreshLayout.setOnRefreshListener(this)
|
||||||
|
|
||||||
logsets.add(getString(R.string.pull_down_to_refresh))
|
toast(getString(R.string.pull_down_to_refresh))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLogcat() {
|
private fun onLogLongClick(log: String): Boolean {
|
||||||
|
Utils.setClipboard(this, log)
|
||||||
try {
|
return true
|
||||||
binding.refreshLayout.isRefreshing = true
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.Default) {
|
|
||||||
val lst = LinkedHashSet<String>()
|
|
||||||
lst.add("logcat")
|
|
||||||
lst.add("-d")
|
|
||||||
lst.add("-v")
|
|
||||||
lst.add("time")
|
|
||||||
lst.add("-s")
|
|
||||||
lst.add("GoLog,${ANG_PACKAGE},AndroidRuntime,System.err")
|
|
||||||
val process = withContext(Dispatchers.IO) {
|
|
||||||
Runtime.getRuntime().exec(lst.toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
val allText = process.inputStream.bufferedReader().use { it.readLines() }.reversed()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
logsetsAll = allText.toMutableList()
|
|
||||||
logsets = allText.toMutableList()
|
|
||||||
refreshData()
|
|
||||||
binding.refreshLayout.isRefreshing = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to get logcat", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearLogcat() {
|
|
||||||
try {
|
|
||||||
lifecycleScope.launch(Dispatchers.Default) {
|
|
||||||
val lst = LinkedHashSet<String>()
|
|
||||||
lst.add("logcat")
|
|
||||||
lst.add("-c")
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
|
||||||
process.waitFor()
|
|
||||||
}
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
logsetsAll.clear()
|
|
||||||
logsets.clear()
|
|
||||||
refreshData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to clear logcat", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
@@ -104,12 +55,14 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
|||||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
filterLogs(newText)
|
viewModel.filter(newText)
|
||||||
|
refreshData()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
searchView.setOnCloseListener {
|
searchView.setOnCloseListener {
|
||||||
filterLogs("")
|
viewModel.filter("")
|
||||||
|
refreshData()
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,33 +72,33 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
R.id.copy_all -> {
|
R.id.copy_all -> {
|
||||||
Utils.setClipboard(this, logsets.joinToString("\n"))
|
val all = viewModel.getAll().joinToString("\n")
|
||||||
|
Utils.setClipboard(this, all)
|
||||||
toastSuccess(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.clear_all -> {
|
R.id.clear_all -> {
|
||||||
clearLogcat()
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
viewModel.clearLogcat()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
refreshData()
|
||||||
|
}
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filterLogs(content: String?): Boolean {
|
|
||||||
val key = content?.trim()
|
|
||||||
logsets = if (key.isNullOrEmpty()) {
|
|
||||||
logsetsAll.toMutableList()
|
|
||||||
} else {
|
|
||||||
logsetsAll.filter { it.contains(key) }.toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshData()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRefresh() {
|
override fun onRefresh() {
|
||||||
getLogcat()
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
viewModel.loadLogcat()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
binding.refreshLayout.isRefreshing = false
|
||||||
|
refreshData()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
|||||||
@@ -6,17 +6,20 @@ import android.view.ViewGroup
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
|
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.viewmodel.LogcatViewModel
|
||||||
|
|
||||||
class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() {
|
class LogcatRecyclerAdapter(
|
||||||
private var mActivity: LogcatActivity = activity
|
private val viewModel: LogcatViewModel,
|
||||||
|
private val onLongClick: ((String) -> Boolean)? = null
|
||||||
|
) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() {
|
||||||
|
|
||||||
|
override fun getItemCount() = viewModel.getAll().size
|
||||||
override fun getItemCount() = mActivity.logsets.size
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||||
try {
|
try {
|
||||||
val log = mActivity.logsets[position]
|
val logs = viewModel.getAll()
|
||||||
|
val log = logs[position]
|
||||||
|
|
||||||
if (log.isEmpty()) {
|
if (log.isEmpty()) {
|
||||||
holder.itemSubSettingBinding.logTag.text = ""
|
holder.itemSubSettingBinding.logTag.text = ""
|
||||||
holder.itemSubSettingBinding.logContent.text = ""
|
holder.itemSubSettingBinding.logContent.text = ""
|
||||||
@@ -27,8 +30,7 @@ class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
holder.itemView.setOnLongClickListener {
|
holder.itemView.setOnLongClickListener {
|
||||||
Utils.setClipboard(mActivity, log)
|
onLongClick?.invoke(log) ?: false
|
||||||
true
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Error binding log view data", e)
|
Log.e(AppConfig.TAG, "Error binding log view data", e)
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.VPN
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityMainBinding
|
import com.v2ray.ang.databinding.ActivityMainBinding
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
@@ -34,6 +33,7 @@ import com.v2ray.ang.extension.toastError
|
|||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsChangeManager
|
import com.v2ray.ang.handler.SettingsChangeManager
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.handler.V2RayServiceManager
|
import com.v2ray.ang.handler.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.viewmodel.MainViewModel
|
import com.v2ray.ang.viewmodel.MainViewModel
|
||||||
@@ -115,52 +115,20 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
setupToolbar(binding.toolbar,false, getString(R.string.title_server))
|
setupToolbar(binding.toolbar, false, getString(R.string.title_server))
|
||||||
|
|
||||||
binding.fab.setOnClickListener {
|
|
||||||
if (mainViewModel.isRunning.value == true) {
|
|
||||||
V2RayServiceManager.stopVService(this)
|
|
||||||
} else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
|
|
||||||
val intent = VpnService.prepare(this)
|
|
||||||
if (intent == null) {
|
|
||||||
startV2Ray()
|
|
||||||
} else {
|
|
||||||
requestVpnPermission.launch(intent)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startV2Ray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.layoutTest.setOnClickListener {
|
|
||||||
if (mainViewModel.isRunning.value == true) {
|
|
||||||
setTestState(getString(R.string.connection_test_testing))
|
|
||||||
mainViewModel.testCurrentServerRealPing()
|
|
||||||
} else {
|
|
||||||
// tv_test_state.text = getString(R.string.connection_test_fail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// setup viewpager and tablayout
|
||||||
groupPagerAdapter = GroupPagerAdapter(this, emptyList())
|
groupPagerAdapter = GroupPagerAdapter(this, emptyList())
|
||||||
binding.viewPager.adapter = groupPagerAdapter
|
binding.viewPager.adapter = groupPagerAdapter
|
||||||
binding.viewPager.isUserInputEnabled = true
|
binding.viewPager.isUserInputEnabled = true
|
||||||
|
|
||||||
|
// setup navigation drawer
|
||||||
val toggle = ActionBarDrawerToggle(
|
val toggle = ActionBarDrawerToggle(
|
||||||
this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
|
this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
|
||||||
)
|
)
|
||||||
binding.drawerLayout.addDrawerListener(toggle)
|
binding.drawerLayout.addDrawerListener(toggle)
|
||||||
toggle.syncState()
|
toggle.syncState()
|
||||||
binding.navView.setNavigationItemSelectedListener(this)
|
binding.navView.setNavigationItemSelectedListener(this)
|
||||||
|
|
||||||
setupGroupTab()
|
|
||||||
setupViewModel()
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
pendingAction = Action.POST_NOTIFICATIONS
|
|
||||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||||
@@ -172,25 +140,26 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
binding.fab.setOnClickListener { handleFabAction() }
|
||||||
|
binding.layoutTest.setOnClickListener { handleLayoutTestClick() }
|
||||||
|
|
||||||
|
setupGroupTab()
|
||||||
|
setupViewModel()
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
pendingAction = Action.POST_NOTIFICATIONS
|
||||||
|
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupViewModel() {
|
private fun setupViewModel() {
|
||||||
mainViewModel.updateTestResultAction.observe(this) { setTestState(it) }
|
mainViewModel.updateTestResultAction.observe(this) { setTestState(it) }
|
||||||
mainViewModel.isRunning.observe(this) { isRunning ->
|
mainViewModel.isRunning.observe(this) { isRunning ->
|
||||||
if (isRunning) {
|
applyRunningState(false, isRunning)
|
||||||
binding.fab.setImageResource(R.drawable.ic_stop_24dp)
|
|
||||||
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_active))
|
|
||||||
binding.fab.contentDescription = getString(R.string.action_stop_service)
|
|
||||||
setTestState(getString(R.string.connection_connected))
|
|
||||||
binding.layoutTest.isFocusable = true
|
|
||||||
} else {
|
|
||||||
binding.fab.setImageResource(R.drawable.ic_play_24dp)
|
|
||||||
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
|
|
||||||
binding.fab.contentDescription = getString(R.string.tasker_start_service)
|
|
||||||
setTestState(getString(R.string.connection_not_connected))
|
|
||||||
binding.layoutTest.isFocusable = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
mainViewModel.startListenBroadcast()
|
mainViewModel.startListenBroadcast()
|
||||||
mainViewModel.initAssets(assets)
|
mainViewModel.initAssets(assets)
|
||||||
@@ -214,6 +183,32 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
binding.tabGroup.isVisible = groups.size > 1
|
binding.tabGroup.isVisible = groups.size > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleFabAction() {
|
||||||
|
applyRunningState(isLoading = true, isRunning = false)
|
||||||
|
|
||||||
|
if (mainViewModel.isRunning.value == true) {
|
||||||
|
V2RayServiceManager.stopVService(this)
|
||||||
|
} else if (SettingsManager.isVpnMode()) {
|
||||||
|
val intent = VpnService.prepare(this)
|
||||||
|
if (intent == null) {
|
||||||
|
startV2Ray()
|
||||||
|
} else {
|
||||||
|
requestVpnPermission.launch(intent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startV2Ray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleLayoutTestClick() {
|
||||||
|
if (mainViewModel.isRunning.value == true) {
|
||||||
|
setTestState(getString(R.string.connection_test_testing))
|
||||||
|
mainViewModel.testCurrentServerRealPing()
|
||||||
|
} else {
|
||||||
|
// service not running: keep existing no-op (could show a message if desired)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun startV2Ray() {
|
private fun startV2Ray() {
|
||||||
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
||||||
toast(R.string.title_file_chooser)
|
toast(R.string.title_file_chooser)
|
||||||
@@ -232,6 +227,31 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setTestState(content: String?) {
|
||||||
|
binding.tvTestState.text = content
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyRunningState(isLoading: Boolean, isRunning: Boolean) {
|
||||||
|
if (isLoading) {
|
||||||
|
binding.fab.setImageResource(R.drawable.ic_fab_check)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
binding.fab.setImageResource(R.drawable.ic_stop_24dp)
|
||||||
|
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_active))
|
||||||
|
binding.fab.contentDescription = getString(R.string.action_stop_service)
|
||||||
|
setTestState(getString(R.string.connection_connected))
|
||||||
|
binding.layoutTest.isFocusable = true
|
||||||
|
} else {
|
||||||
|
binding.fab.setImageResource(R.drawable.ic_play_24dp)
|
||||||
|
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
|
||||||
|
binding.fab.contentDescription = getString(R.string.tasker_start_service)
|
||||||
|
setTestState(getString(R.string.connection_not_connected))
|
||||||
|
binding.layoutTest.isFocusable = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
@@ -613,19 +633,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setTestState(content: String?) {
|
|
||||||
binding.tvTestState.text = content
|
|
||||||
}
|
|
||||||
|
|
||||||
// val mConnection = object : ServiceConnection {
|
|
||||||
// override fun onServiceDisconnected(name: ComponentName?) {
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
|
||||||
// sendMsg(AppConfig.MSG_REGISTER_CLIENT, "")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) {
|
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) {
|
||||||
moveTaskToBack(false)
|
moveTaskToBack(false)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
|||||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -282,6 +283,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
private fun removeServer(guid: String, position: Int) {
|
private fun removeServer(guid: String, position: Int) {
|
||||||
if (guid == MmkvManager.getSelectServer()) {
|
if (guid == MmkvManager.getSelectServer()) {
|
||||||
application.toast(R.string.toast_action_not_allowed)
|
application.toast(R.string.toast_action_not_allowed)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
|
||||||
@@ -368,6 +370,9 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
|
|
||||||
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
||||||
mActivity.mainViewModel.swapServer(fromPosition, toPosition)
|
mActivity.mainViewModel.swapServer(fromPosition, toPosition)
|
||||||
|
if (fromPosition < data.size && toPosition < data.size) {
|
||||||
|
Collections.swap(data, fromPosition, toPosition)
|
||||||
|
}
|
||||||
notifyItemMoved(fromPosition, toPosition)
|
notifyItemMoved(fromPosition, toPosition)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
class RoutingSettingActivity : BaseActivity() {
|
class RoutingSettingActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivityRoutingSettingBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityRoutingSettingBinding.inflate(layoutInflater) }
|
||||||
|
private val ownerActivity: RoutingSettingActivity
|
||||||
|
get() = this
|
||||||
private val viewModel: RoutingSettingsViewModel by viewModels()
|
private val viewModel: RoutingSettingsViewModel by viewModels()
|
||||||
private lateinit var adapter: RoutingSettingRecyclerAdapter
|
private lateinit var adapter: RoutingSettingRecyclerAdapter
|
||||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||||
@@ -57,7 +58,7 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
//setContentView(binding.root)
|
//setContentView(binding.root)
|
||||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.routing_settings_title))
|
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.routing_settings_title))
|
||||||
|
|
||||||
adapter = RoutingSettingRecyclerAdapter(this, viewModel)
|
adapter = RoutingSettingRecyclerAdapter(viewModel, ActivityAdapterListener())
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
@@ -202,4 +203,23 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
viewModel.reload()
|
viewModel.reload()
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class ActivityAdapterListener : BaseAdapterListener {
|
||||||
|
override fun onEdit(guid: String, position: Int) {
|
||||||
|
startActivity(
|
||||||
|
Intent(ownerActivity, RoutingEditActivity::class.java)
|
||||||
|
.putExtra("position", position)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemove(guid: String, position: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShare(url: String) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRefreshData() {
|
||||||
|
refreshData()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -13,12 +12,11 @@ import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
|||||||
import com.v2ray.ang.viewmodel.RoutingSettingsViewModel
|
import com.v2ray.ang.viewmodel.RoutingSettingsViewModel
|
||||||
|
|
||||||
class RoutingSettingRecyclerAdapter(
|
class RoutingSettingRecyclerAdapter(
|
||||||
val activity: RoutingSettingActivity,
|
private val viewModel: RoutingSettingsViewModel,
|
||||||
private val viewModel: RoutingSettingsViewModel
|
private val adapterListener: BaseAdapterListener?
|
||||||
) : RecyclerView.Adapter<RoutingSettingRecyclerAdapter.MainViewHolder>(),
|
) : RecyclerView.Adapter<RoutingSettingRecyclerAdapter.MainViewHolder>(),
|
||||||
ItemTouchHelperAdapter {
|
ItemTouchHelperAdapter {
|
||||||
|
|
||||||
private var mActivity: RoutingSettingActivity = activity
|
|
||||||
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) {
|
||||||
@@ -33,10 +31,7 @@ class RoutingSettingRecyclerAdapter(
|
|||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener {
|
holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener {
|
||||||
mActivity.startActivity(
|
adapterListener?.onEdit("", position)
|
||||||
Intent(mActivity, RoutingEditActivity::class.java)
|
|
||||||
.putExtra("position", position)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
||||||
@@ -76,7 +71,7 @@ class RoutingSettingRecyclerAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemMoveCompleted() {
|
override fun onItemMoveCompleted() {
|
||||||
mActivity.refreshData()
|
adapterListener?.onRefreshData()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemDismiss(position: Int) {
|
override fun onItemDismiss(position: Int) {
|
||||||
|
|||||||
@@ -21,23 +21,18 @@ import com.v2ray.ang.util.Utils
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class SettingsActivity : BaseActivity() {
|
class SettingsActivity : BaseActivity() {
|
||||||
//private val settingsViewModel: SettingsViewModel by viewModels()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
//setContentView(R.layout.activity_settings)
|
|
||||||
setContentViewWithToolbar(R.layout.activity_settings, showHomeAsUp = true, title = getString(R.string.title_settings))
|
setContentViewWithToolbar(R.layout.activity_settings, showHomeAsUp = true, title = getString(R.string.title_settings))
|
||||||
|
|
||||||
//settingsViewModel.startListenPreferenceChange()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsFragment : PreferenceFragmentCompat() {
|
class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
// private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) }
|
|
||||||
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
|
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
|
||||||
private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
|
private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
|
||||||
private val appendHttpProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_APPEND_HTTP_PROXY) }
|
private val appendHttpProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_APPEND_HTTP_PROXY) }
|
||||||
// private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
|
|
||||||
|
// private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
|
||||||
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
|
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
|
||||||
private val vpnBypassLan by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_BYPASS_LAN) }
|
private val vpnBypassLan by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_BYPASS_LAN) }
|
||||||
private val vpnInterfaceAddress by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX) }
|
private val vpnInterfaceAddress by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX) }
|
||||||
@@ -55,18 +50,11 @@ class SettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
private val autoUpdateCheck by lazy { findPreference<CheckBoxPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE) }
|
private val autoUpdateCheck by lazy { findPreference<CheckBoxPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE) }
|
||||||
private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
|
private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
|
||||||
|
|
||||||
// private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
|
|
||||||
// private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) }
|
|
||||||
// private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_DNS) }
|
|
||||||
// private val dnsHosts by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DNS_HOSTS) }
|
|
||||||
// private val delayTestUrl by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DELAY_TEST_URL) }
|
|
||||||
// private val ipApiUrl by lazy { findPreference<EditTextPreference>(AppConfig.PREF_IP_API_URL) }
|
|
||||||
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
||||||
|
|
||||||
// private val hevTunLogLevel by lazy { findPreference<ListPreference>(AppConfig.PREF_HEV_TUNNEL_LOGLEVEL) }
|
private val hevTunLogLevel by lazy { findPreference<ListPreference>(AppConfig.PREF_HEV_TUNNEL_LOGLEVEL) }
|
||||||
// private val hevTunRwTimeout by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT) }
|
private val hevTunRwTimeout by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT) }
|
||||||
// private val useTun by lazy { findPreference<ListPreference>(AppConfig.PREF_TUN) }
|
private val useHevTun by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_USE_HEV_TUNNEL) }
|
||||||
|
|
||||||
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
|
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
|
||||||
// Use MMKV as the storage backend for all Preferences
|
// Use MMKV as the storage backend for all Preferences
|
||||||
@@ -77,30 +65,10 @@ class SettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
initPreferenceSummaries()
|
initPreferenceSummaries()
|
||||||
|
|
||||||
// perAppProxy?.setOnPreferenceClickListener {
|
|
||||||
// startActivity(Intent(activity, PerAppProxyActivity::class.java))
|
|
||||||
// perAppProxy?.isChecked = true
|
|
||||||
// false
|
|
||||||
// }
|
|
||||||
localDns?.setOnPreferenceChangeListener { _, any ->
|
localDns?.setOnPreferenceChangeListener { _, any ->
|
||||||
updateLocalDns(any as Boolean)
|
updateLocalDns(any as Boolean)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
// localDnsPort?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
// val nval = any as String
|
|
||||||
// localDnsPort?.summary = nval.ifEmpty { AppConfig.PORT_LOCAL_DNS }
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
// vpnDns?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
// vpnDns?.summary = any as String
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
|
|
||||||
// vpnMtu?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
// val nval = any as String
|
|
||||||
// vpnMtu?.summary = nval.ifEmpty { AppConfig.VPN_MTU.toString() }
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
|
|
||||||
mux?.setOnPreferenceChangeListener { _, newValue ->
|
mux?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
updateMux(newValue as Boolean)
|
updateMux(newValue as Boolean)
|
||||||
@@ -119,18 +87,6 @@ class SettingsActivity : BaseActivity() {
|
|||||||
updateFragment(newValue as Boolean)
|
updateFragment(newValue as Boolean)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
// fragmentPackets?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
// updateFragmentPackets(newValue as String)
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
// fragmentLength?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
// updateFragmentLength(newValue as String)
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
// fragmentInterval?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
// updateFragmentInterval(newValue as String)
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
|
|
||||||
autoUpdateCheck?.setOnPreferenceChangeListener { _, newValue ->
|
autoUpdateCheck?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
val value = newValue as Boolean
|
val value = newValue as Boolean
|
||||||
@@ -141,65 +97,21 @@ class SettingsActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
// autoUpdateInterval?.setOnPreferenceChangeListener { _, any ->
|
mode?.setOnPreferenceChangeListener { pref, newValue ->
|
||||||
// var nval = any as String
|
val valueStr = newValue.toString()
|
||||||
//
|
(pref as? ListPreference)?.let { lp ->
|
||||||
// // It must be greater than 15 minutes because WorkManager couldn't run tasks under 15 minutes intervals
|
val idx = lp.findIndexOfValue(valueStr)
|
||||||
// nval =
|
lp.summary = if (idx >= 0) lp.entries[idx] else valueStr
|
||||||
// if (TextUtils.isEmpty(nval) || nval.toLongEx() < 15) AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL else nval
|
}
|
||||||
// autoUpdateInterval?.summary = nval
|
updateMode(valueStr)
|
||||||
// configureUpdateTask(nval.toLongEx())
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
|
|
||||||
// socksPort?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
// val nval = any as String
|
|
||||||
// socksPort?.summary = nval.ifEmpty { AppConfig.PORT_SOCKS }
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// remoteDns?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
// val nval = any as String
|
|
||||||
// remoteDns?.summary = nval.ifEmpty { AppConfig.DNS_PROXY }
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
// domesticDns?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
// val nval = any as String
|
|
||||||
// domesticDns?.summary = nval.ifEmpty { AppConfig.DNS_DIRECT }
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
// dnsHosts?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
// val nval = any as String
|
|
||||||
// dnsHosts?.summary = nval
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
// delayTestUrl?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
// val nval = any as String
|
|
||||||
// delayTestUrl?.summary = nval.ifEmpty { AppConfig.DELAY_TEST_URL }
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
// ipApiUrl?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
// val nval = any as String
|
|
||||||
// ipApiUrl?.summary = nval.ifEmpty { AppConfig.IP_API_URL }
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
mode?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
updateMode(newValue.toString())
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
mode?.dialogLayoutResource = R.layout.preference_with_help_link
|
mode?.dialogLayoutResource = R.layout.preference_with_help_link
|
||||||
//loglevel.summary = "LogLevel"
|
|
||||||
|
|
||||||
// useTun?.setOnPreferenceChangeListener { _, newValue ->
|
useHevTun?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
// updateHevTunSettings(newValue as String == AppConfig.TUN_hevsocks5)
|
updateHevTunSettings(newValue as Boolean)
|
||||||
// true
|
true
|
||||||
// }
|
}
|
||||||
|
|
||||||
// hevTunRwTimeout?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
// val nval = any as String
|
|
||||||
// hevTunRwTimeout?.summary = nval.ifEmpty { AppConfig.HEVTUN_RW_TIMEOUT }
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initPreferenceSummaries() {
|
private fun initPreferenceSummaries() {
|
||||||
@@ -212,6 +124,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ListPreference -> {
|
is ListPreference -> {
|
||||||
pref.summary = pref.entry ?: ""
|
pref.summary = pref.entry ?: ""
|
||||||
pref.setOnPreferenceChangeListener { p, newValue ->
|
pref.setOnPreferenceChangeListener { p, newValue ->
|
||||||
@@ -221,6 +134,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is CheckBoxPreference, is androidx.preference.SwitchPreferenceCompat -> {
|
is CheckBoxPreference, is androidx.preference.SwitchPreferenceCompat -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,6 +154,8 @@ class SettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
|
updateHevTunSettings(MmkvManager.decodeSettingsBool(AppConfig.PREF_USE_HEV_TUNNEL, true))
|
||||||
|
|
||||||
// Initialize mode-dependent UI states
|
// Initialize mode-dependent UI states
|
||||||
updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
|
updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
|
||||||
|
|
||||||
@@ -251,101 +167,10 @@ class SettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
// Initialize auto-update interval state
|
// Initialize auto-update interval state
|
||||||
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
||||||
|
|
||||||
// localDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
|
||||||
// fakeDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED, false)
|
|
||||||
// appendHttpProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY, false)
|
|
||||||
// vpnDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
|
|
||||||
// vpnMtu?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_MTU, AppConfig.VPN_MTU.toString())
|
|
||||||
// mux?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
|
|
||||||
// muxConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8")
|
|
||||||
// muxXudpConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
|
|
||||||
// fragment?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false)
|
|
||||||
// fragmentPackets?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")
|
|
||||||
// fragmentLength?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
|
|
||||||
// fragmentInterval?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
|
|
||||||
// autoUpdateCheck?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
|
||||||
// autoUpdateInterval?.summary =
|
|
||||||
// MmkvManager.decodeSettingsString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
|
|
||||||
// socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
|
||||||
// remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
|
|
||||||
// domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
|
|
||||||
// dnsHosts?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
|
|
||||||
// delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DELAY_TEST_URL)
|
|
||||||
// ipApiUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_IP_API_URL, AppConfig.IP_API_URL)
|
|
||||||
// hevTunRwTimeout?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT, AppConfig.HEVTUN_RW_TIMEOUT)
|
|
||||||
// updateHevTunSettings(MmkvManager.decodeSettingsString(AppConfig.PREF_TUN, AppConfig.TUN_hevsocks5) == AppConfig.TUN_hevsocks5)
|
|
||||||
|
|
||||||
// initSharedPreference()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initSharedPreference() {
|
private fun updateMode(value: String?) {
|
||||||
// listOf(
|
val vpn = value == VPN
|
||||||
// //localDnsPort,
|
|
||||||
// vpnDns,
|
|
||||||
// vpnMtu,
|
|
||||||
// muxConcurrency,
|
|
||||||
// muxXudpConcurrency,
|
|
||||||
// fragmentLength,
|
|
||||||
// fragmentInterval,
|
|
||||||
// autoUpdateInterval,
|
|
||||||
// socksPort,
|
|
||||||
// remoteDns,
|
|
||||||
// domesticDns,
|
|
||||||
// delayTestUrl,
|
|
||||||
// ipApiUrl,
|
|
||||||
// hevTunRwTimeout
|
|
||||||
// ).forEach { key ->
|
|
||||||
// key?.summary = key.text.toString()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// listOf(
|
|
||||||
// AppConfig.PREF_SNIFFING_ENABLED,
|
|
||||||
// AppConfig.PREF_USE_HEV_TUNNEL
|
|
||||||
// ).forEach { key ->
|
|
||||||
// findPreference<CheckBoxPreference>(key)?.isChecked =
|
|
||||||
// MmkvManager.decodeSettingsBool(key, true)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// listOf(
|
|
||||||
// AppConfig.PREF_ROUTE_ONLY_ENABLED,
|
|
||||||
// AppConfig.PREF_IS_BOOTED,
|
|
||||||
// AppConfig.PREF_BYPASS_APPS,
|
|
||||||
// AppConfig.PREF_SPEED_ENABLED,
|
|
||||||
// AppConfig.PREF_CONFIRM_REMOVE,
|
|
||||||
// AppConfig.PREF_START_SCAN_IMMEDIATE,
|
|
||||||
// AppConfig.PREF_DOUBLE_COLUMN_DISPLAY,
|
|
||||||
// AppConfig.PREF_PREFER_IPV6,
|
|
||||||
// AppConfig.PREF_PROXY_SHARING,
|
|
||||||
// AppConfig.PREF_ALLOW_INSECURE
|
|
||||||
// ).forEach { key ->
|
|
||||||
// findPreference<CheckBoxPreference>(key)?.isChecked =
|
|
||||||
// MmkvManager.decodeSettingsBool(key, false)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// listOf(
|
|
||||||
// AppConfig.PREF_VPN_BYPASS_LAN,
|
|
||||||
// AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX,
|
|
||||||
// AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
|
|
||||||
// AppConfig.PREF_MUX_XUDP_QUIC,
|
|
||||||
// AppConfig.PREF_FRAGMENT_PACKETS,
|
|
||||||
// AppConfig.PREF_LANGUAGE,
|
|
||||||
// AppConfig.PREF_UI_MODE_NIGHT,
|
|
||||||
// AppConfig.PREF_LOGLEVEL,
|
|
||||||
// AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD,
|
|
||||||
// AppConfig.PREF_MODE,
|
|
||||||
// AppConfig.PREF_HEV_TUNNEL_LOGLEVEL
|
|
||||||
// ).forEach { key ->
|
|
||||||
// if (MmkvManager.decodeSettingsString(key) != null) {
|
|
||||||
// findPreference<ListPreference>(key)?.value = MmkvManager.decodeSettingsString(key)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateMode(mode: String?) {
|
|
||||||
val vpn = mode == VPN
|
|
||||||
// perAppProxy?.isEnabled = vpn
|
|
||||||
// perAppProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false)
|
|
||||||
localDns?.isEnabled = vpn
|
localDns?.isEnabled = vpn
|
||||||
fakeDns?.isEnabled = vpn
|
fakeDns?.isEnabled = vpn
|
||||||
appendHttpProxy?.isEnabled = vpn
|
appendHttpProxy?.isEnabled = vpn
|
||||||
@@ -354,6 +179,8 @@ class SettingsActivity : BaseActivity() {
|
|||||||
vpnBypassLan?.isEnabled = vpn
|
vpnBypassLan?.isEnabled = vpn
|
||||||
vpnInterfaceAddress?.isEnabled = vpn
|
vpnInterfaceAddress?.isEnabled = vpn
|
||||||
vpnMtu?.isEnabled = vpn
|
vpnMtu?.isEnabled = vpn
|
||||||
|
useHevTun?.isEnabled = vpn
|
||||||
|
updateHevTunSettings(false)
|
||||||
if (vpn) {
|
if (vpn) {
|
||||||
updateLocalDns(
|
updateLocalDns(
|
||||||
MmkvManager.decodeSettingsBool(
|
MmkvManager.decodeSettingsBool(
|
||||||
@@ -361,6 +188,12 @@ class SettingsActivity : BaseActivity() {
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
updateHevTunSettings(
|
||||||
|
MmkvManager.decodeSettingsBool(
|
||||||
|
AppConfig.PREF_USE_HEV_TUNNEL,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,29 +256,12 @@ class SettingsActivity : BaseActivity() {
|
|||||||
fragmentPackets?.isEnabled = enabled
|
fragmentPackets?.isEnabled = enabled
|
||||||
fragmentLength?.isEnabled = enabled
|
fragmentLength?.isEnabled = enabled
|
||||||
fragmentInterval?.isEnabled = enabled
|
fragmentInterval?.isEnabled = enabled
|
||||||
// if (enabled) {
|
|
||||||
// updateFragmentPackets(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello"))
|
|
||||||
// updateFragmentLength(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100"))
|
|
||||||
// updateFragmentInterval(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
//
|
|
||||||
// private fun updateFragmentPackets(value: String?) {
|
private fun updateHevTunSettings(enabled: Boolean) {
|
||||||
// fragmentPackets?.summary = value.toString()
|
hevTunLogLevel?.isEnabled = enabled
|
||||||
// }
|
hevTunRwTimeout?.isEnabled = enabled
|
||||||
//
|
}
|
||||||
// private fun updateFragmentLength(value: String?) {
|
|
||||||
// fragmentLength?.summary = value.toString()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun updateFragmentInterval(value: String?) {
|
|
||||||
// fragmentInterval?.summary = value.toString()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun updateHevTunSettings(enabled: Boolean) {
|
|
||||||
// hevTunLogLevel?.isEnabled = enabled
|
|
||||||
// hevTunRwTimeout?.isEnabled = enabled
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onModeHelpClicked(view: View) {
|
fun onModeHelpClicked(view: View) {
|
||||||
|
|||||||
@@ -3,18 +3,27 @@ package com.v2ray.ang.ui
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
||||||
|
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastError
|
import com.v2ray.ang.extension.toastError
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.viewmodel.SubscriptionsViewModel
|
import com.v2ray.ang.viewmodel.SubscriptionsViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -22,17 +31,21 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
class SubSettingActivity : BaseActivity() {
|
class SubSettingActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
|
||||||
|
private val ownerActivity: SubSettingActivity
|
||||||
|
get() = this
|
||||||
private val viewModel: SubscriptionsViewModel by viewModels()
|
private val viewModel: SubscriptionsViewModel by viewModels()
|
||||||
private lateinit var adapter: SubSettingRecyclerAdapter
|
private lateinit var adapter: SubSettingRecyclerAdapter
|
||||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||||
|
private val share_method: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.share_sub_method)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
//setContentView(binding.root)
|
//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(this, viewModel)
|
adapter = SubSettingRecyclerAdapter(viewModel, ActivityAdapterListener())
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
@@ -88,4 +101,62 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
viewModel.reload()
|
viewModel.reload()
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class ActivityAdapterListener : BaseAdapterListener {
|
||||||
|
override fun onEdit(guid: String, position: Int) {
|
||||||
|
startActivity(
|
||||||
|
Intent(ownerActivity, SubEditActivity::class.java)
|
||||||
|
.putExtra("subId", guid)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemove(guid: String, position: Int) {
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
|
||||||
|
AlertDialog.Builder(ownerActivity)
|
||||||
|
.setMessage(R.string.del_config_comfirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
viewModel.remove(guid)
|
||||||
|
refreshData()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
viewModel.remove(guid)
|
||||||
|
refreshData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShare(url: String) {
|
||||||
|
AlertDialog.Builder(ownerActivity)
|
||||||
|
.setItems(share_method.asList().toTypedArray()) { _, i ->
|
||||||
|
try {
|
||||||
|
when (i) {
|
||||||
|
0 -> {
|
||||||
|
val ivBinding =
|
||||||
|
ItemQrcodeBinding.inflate(LayoutInflater.from(ownerActivity))
|
||||||
|
ivBinding.ivQcode.setImageBitmap(
|
||||||
|
QRCodeDecoder.createQRCode(
|
||||||
|
url
|
||||||
|
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AlertDialog.Builder(ownerActivity).setView(ivBinding.root).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
Utils.setClipboard(ownerActivity, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> ownerActivity.toast("else")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Share subscription failed", e)
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRefreshData() {
|
||||||
|
refreshData()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,22 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
|
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.viewmodel.SubscriptionsViewModel
|
import com.v2ray.ang.viewmodel.SubscriptionsViewModel
|
||||||
|
|
||||||
class SubSettingRecyclerAdapter(
|
class SubSettingRecyclerAdapter(
|
||||||
val activity: SubSettingActivity,
|
private val viewModel: SubscriptionsViewModel,
|
||||||
private val viewModel: SubscriptionsViewModel
|
private val adapterListener: BaseAdapterListener?
|
||||||
) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter {
|
) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter {
|
||||||
|
|
||||||
private var mActivity: SubSettingActivity = activity
|
|
||||||
|
|
||||||
private val share_method: Array<out String> by lazy {
|
|
||||||
mActivity.resources.getStringArray(R.array.share_sub_method)
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
||||||
@@ -45,14 +30,11 @@ class SubSettingRecyclerAdapter(
|
|||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
||||||
mActivity.startActivity(
|
adapterListener?.onEdit(subId, position)
|
||||||
Intent(mActivity, SubEditActivity::class.java)
|
|
||||||
.putExtra("subId", subId)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
||||||
removeSubscription(subId, position)
|
adapterListener?.onRemove(subId, position)
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
||||||
@@ -72,58 +54,11 @@ class SubSettingRecyclerAdapter(
|
|||||||
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.setOnClickListener {
|
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
||||||
AlertDialog.Builder(mActivity)
|
adapterListener?.onShare(subItem.url)
|
||||||
.setItems(share_method.asList().toTypedArray()) { _, i ->
|
|
||||||
try {
|
|
||||||
when (i) {
|
|
||||||
0 -> {
|
|
||||||
val ivBinding =
|
|
||||||
ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
|
|
||||||
ivBinding.ivQcode.setImageBitmap(
|
|
||||||
QRCodeDecoder.createQRCode(
|
|
||||||
subItem.url
|
|
||||||
|
|
||||||
)
|
|
||||||
)
|
|
||||||
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
1 -> {
|
|
||||||
Utils.setClipboard(mActivity, subItem.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> mActivity.toast("else")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Share subscription failed", e)
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeSubscription(subId: String, position: Int) {
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
|
|
||||||
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
removeSubscriptionSub(subId, position)
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
} else {
|
|
||||||
removeSubscriptionSub(subId, position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeSubscriptionSub(subId: String, position: Int) {
|
|
||||||
viewModel.remove(subId)
|
|
||||||
notifyItemRemoved(position)
|
|
||||||
notifyItemRangeChanged(position, viewModel.getAll().size)
|
|
||||||
mActivity.refreshData()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
||||||
return MainViewHolder(
|
return MainViewHolder(
|
||||||
ItemRecyclerSubSettingBinding.inflate(
|
ItemRecyclerSubSettingBinding.inflate(
|
||||||
@@ -154,7 +89,7 @@ class SubSettingRecyclerAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemMoveCompleted() {
|
override fun onItemMoveCompleted() {
|
||||||
mActivity.refreshData()
|
adapterListener?.onRefreshData()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemDismiss(position: Int) {
|
override fun onItemDismiss(position: Int) {
|
||||||
|
|||||||
@@ -9,44 +9,36 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View.GONE
|
|
||||||
import android.view.View.VISIBLE
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityUserAssetBinding
|
import com.v2ray.ang.databinding.ActivityUserAssetBinding
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
|
||||||
import com.v2ray.ang.dto.AssetUrlItem
|
import com.v2ray.ang.dto.AssetUrlItem
|
||||||
import com.v2ray.ang.extension.concatUrl
|
|
||||||
import com.v2ray.ang.extension.toTrafficString
|
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastError
|
import com.v2ray.ang.extension.toastError
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.HttpUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
import com.v2ray.ang.viewmodel.UserAssetViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
class UserAssetActivity : BaseActivity() {
|
class UserAssetActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivityUserAssetBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityUserAssetBinding.inflate(layoutInflater) }
|
||||||
|
private val ownerActivity: UserAssetActivity
|
||||||
|
get() = this
|
||||||
|
private val viewModel: UserAssetViewModel by viewModels()
|
||||||
|
private lateinit var adapter: UserAssetAdapter
|
||||||
|
|
||||||
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
||||||
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
|
|
||||||
|
|
||||||
private val requestStoragePermissionLauncher = registerForActivityResult(
|
private val requestStoragePermissionLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.RequestPermission()
|
ActivityResultContracts.RequestPermission()
|
||||||
@@ -83,13 +75,13 @@ class UserAssetActivity : 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_user_asset_setting))
|
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_user_asset_setting))
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
binding.recyclerView.adapter = UserAssetAdapter()
|
adapter = UserAssetAdapter(viewModel, extDir, ActivityAdapterListener())
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources()
|
binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources()
|
||||||
binding.layoutGeoFilesSources.setOnClickListener {
|
binding.layoutGeoFilesSources.setOnClickListener {
|
||||||
@@ -219,81 +211,25 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadGeoFiles() {
|
private fun downloadGeoFiles() {
|
||||||
|
refreshData()
|
||||||
showLoading()
|
showLoading()
|
||||||
toast(R.string.msg_downloading_content)
|
toast(R.string.msg_downloading_content)
|
||||||
|
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
var assets = MmkvManager.decodeAssetUrls()
|
|
||||||
assets = addBuiltInGeoItems(assets)
|
|
||||||
|
|
||||||
var resultCount = 0
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
assets.forEach {
|
val result = viewModel.downloadGeoFiles(extDir, httpPort)
|
||||||
try {
|
|
||||||
var result = downloadGeo(it.second, 15000, httpPort)
|
|
||||||
if (!result) {
|
|
||||||
result = downloadGeo(it.second, 15000, 0)
|
|
||||||
}
|
|
||||||
if (result)
|
|
||||||
resultCount++
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to download geo file: ${it.second.remarks}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (resultCount > 0) {
|
if (result.successCount > 0) {
|
||||||
toast(getString(R.string.title_update_config_count, resultCount))
|
toast(getString(R.string.title_update_config_count, result.successCount))
|
||||||
refreshData()
|
|
||||||
} else {
|
} else {
|
||||||
toast(getString(R.string.toast_failure))
|
toast(getString(R.string.toast_failure))
|
||||||
}
|
}
|
||||||
|
refreshData()
|
||||||
hideLoading()
|
hideLoading()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
|
|
||||||
val targetTemp = File(extDir, item.remarks + "_temp")
|
|
||||||
val target = File(extDir, item.remarks)
|
|
||||||
Log.i(AppConfig.TAG, "Downloading geo file: ${item.remarks} from ${item.url}")
|
|
||||||
|
|
||||||
val conn = HttpUtil.createProxyConnection(item.url, httpPort, timeout, timeout, needStream = true) ?: return false
|
|
||||||
try {
|
|
||||||
val inputStream = conn.inputStream
|
|
||||||
val responseCode = conn.responseCode
|
|
||||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
|
||||||
FileOutputStream(targetTemp).use { output ->
|
|
||||||
inputStream.copyTo(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
targetTemp.renameTo(target)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
conn.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addBuiltInGeoItems(assets: List<Pair<String, AssetUrlItem>>): List<Pair<String, AssetUrlItem>> {
|
|
||||||
val list = mutableListOf<Pair<String, AssetUrlItem>>()
|
|
||||||
builtInGeoFiles
|
|
||||||
.filter { geoFile -> assets.none { it.second.remarks == geoFile } }
|
|
||||||
.forEach {
|
|
||||||
list.add(
|
|
||||||
Utils.getUuid() to AssetUrlItem(
|
|
||||||
it,
|
|
||||||
String.format(AppConfig.GITHUB_DOWNLOAD_URL, getGeoFilesSources()).concatUrl(it),
|
|
||||||
locked = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return list + assets
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initAssets() {
|
fun initAssets() {
|
||||||
lifecycleScope.launch(Dispatchers.Default) {
|
lifecycleScope.launch(Dispatchers.Default) {
|
||||||
SettingsManager.initAssets(this@UserAssetActivity, assets)
|
SettingsManager.initAssets(this@UserAssetActivity, assets)
|
||||||
@@ -305,72 +241,41 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun refreshData() {
|
fun refreshData() {
|
||||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
viewModel.reload(getGeoFilesSources())
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
|
private inner class ActivityAdapterListener : BaseAdapterListener {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
override fun onEdit(guid: String, position: Int) {
|
||||||
return UserAssetViewHolder(
|
startActivity(
|
||||||
ItemRecyclerUserAssetBinding.inflate(
|
Intent(ownerActivity, UserAssetUrlActivity::class.java)
|
||||||
LayoutInflater.from(parent.context),
|
.putExtra("assetId", guid)
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
override fun onRemove(guid: String, position: Int) {
|
||||||
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
|
val asset = viewModel.getAsset(position)?.takeIf { it.first == guid }
|
||||||
var assets = MmkvManager.decodeAssetUrls()
|
?: viewModel.getAssets().find { it.first == guid }
|
||||||
assets = addBuiltInGeoItems(assets)
|
?: return
|
||||||
val item = assets.getOrNull(position) ?: return
|
val file = extDir.listFiles()?.find { it.name == asset.second.remarks }
|
||||||
// file with name == item.second.remarks
|
|
||||||
val file = extDir.listFiles()?.find { it.name == item.second.remarks }
|
|
||||||
|
|
||||||
holder.itemUserAssetBinding.assetName.text = item.second.remarks
|
AlertDialog.Builder(ownerActivity).setMessage(R.string.del_config_comfirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
if (file != null) {
|
file?.delete()
|
||||||
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
MmkvManager.removeAssetUrl(guid)
|
||||||
holder.itemUserAssetBinding.assetProperties.text =
|
initAssets()
|
||||||
"${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}"
|
}
|
||||||
} else {
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found)
|
// do nothing
|
||||||
}
|
}
|
||||||
|
.show()
|
||||||
if (item.second.locked == true) {
|
|
||||||
holder.itemUserAssetBinding.layoutEdit.visibility = GONE
|
|
||||||
//holder.itemUserAssetBinding.layoutRemove.visibility = GONE
|
|
||||||
} else {
|
|
||||||
holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE }
|
|
||||||
//holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.itemUserAssetBinding.layoutEdit.setOnClickListener {
|
|
||||||
val intent = Intent(this@UserAssetActivity, UserAssetUrlActivity::class.java)
|
|
||||||
intent.putExtra("assetId", item.first)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
|
|
||||||
AlertDialog.Builder(this@UserAssetActivity).setMessage(R.string.del_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
file?.delete()
|
|
||||||
MmkvManager.removeAssetUrl(item.first)
|
|
||||||
initAssets()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun onShare(url: String) {
|
||||||
var assets = MmkvManager.decodeAssetUrls()
|
}
|
||||||
assets = addBuiltInGeoItems(assets)
|
|
||||||
return assets.size
|
override fun onRefreshData() {
|
||||||
|
refreshData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
|
|
||||||
RecyclerView.ViewHolder(itemUserAssetBinding.root)
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
||||||
|
import com.v2ray.ang.extension.toTrafficString
|
||||||
|
import com.v2ray.ang.viewmodel.UserAssetViewModel
|
||||||
|
import java.io.File
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class UserAssetAdapter(
|
||||||
|
private val viewModel: UserAssetViewModel,
|
||||||
|
private val extDir: File,
|
||||||
|
private val adapterListener: BaseAdapterListener?
|
||||||
|
) : RecyclerView.Adapter<UserAssetAdapter.UserAssetViewHolder>() {
|
||||||
|
|
||||||
|
override fun getItemCount() = viewModel.itemCount
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
||||||
|
return UserAssetViewHolder(
|
||||||
|
ItemRecyclerUserAssetBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
|
||||||
|
val item = viewModel.getAsset(position) ?: return
|
||||||
|
val file = extDir.listFiles()?.find { it.name == item.second.remarks }
|
||||||
|
|
||||||
|
holder.itemUserAssetBinding.assetName.text = item.second.remarks
|
||||||
|
|
||||||
|
if (file != null) {
|
||||||
|
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
||||||
|
holder.itemUserAssetBinding.assetProperties.text =
|
||||||
|
"${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}"
|
||||||
|
} else {
|
||||||
|
holder.itemUserAssetBinding.assetProperties.text =
|
||||||
|
holder.itemUserAssetBinding.root.context.getString(R.string.msg_file_not_found)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.second.locked == true) {
|
||||||
|
holder.itemUserAssetBinding.layoutEdit.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
holder.itemUserAssetBinding.layoutEdit.visibility = if (item.second.url == "file") {
|
||||||
|
View.GONE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.itemUserAssetBinding.layoutEdit.setOnClickListener {
|
||||||
|
adapterListener?.onEdit(item.first, position)
|
||||||
|
}
|
||||||
|
holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
|
||||||
|
adapterListener?.onRemove(item.first, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
|
||||||
|
RecyclerView.ViewHolder(itemUserAssetBinding.root)
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.v2ray.ang.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class LogcatViewModel : ViewModel() {
|
||||||
|
private val logsetsAll: MutableList<String> = mutableListOf()
|
||||||
|
private var filteredLogs: List<String> = emptyList()
|
||||||
|
private var currentFilter: String = ""
|
||||||
|
|
||||||
|
fun getAll(): List<String> = filteredLogs
|
||||||
|
|
||||||
|
fun loadLogcat() {
|
||||||
|
try {
|
||||||
|
val lst = LinkedHashSet<String>()
|
||||||
|
lst.add("logcat")
|
||||||
|
lst.add("-d")
|
||||||
|
lst.add("-v")
|
||||||
|
lst.add("time")
|
||||||
|
lst.add("-s")
|
||||||
|
lst.add("GoLog,${ANG_PACKAGE},AndroidRuntime,System.err")
|
||||||
|
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
||||||
|
val allText = process.inputStream.bufferedReader().use { it.readLines() }.reversed()
|
||||||
|
|
||||||
|
logsetsAll.clear()
|
||||||
|
logsetsAll.addAll(allText)
|
||||||
|
applyFilter()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
android.util.Log.e(AppConfig.TAG, "Failed to get logcat", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearLogcat() {
|
||||||
|
try {
|
||||||
|
val lst = LinkedHashSet<String>()
|
||||||
|
lst.add("logcat")
|
||||||
|
lst.add("-c")
|
||||||
|
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
||||||
|
process.waitFor()
|
||||||
|
|
||||||
|
logsetsAll.clear()
|
||||||
|
filteredLogs = emptyList()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
android.util.Log.e(AppConfig.TAG, "Failed to clear logcat", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun filter(content: String?) {
|
||||||
|
currentFilter = content?.trim() ?: ""
|
||||||
|
applyFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyFilter() {
|
||||||
|
filteredLogs = if (currentFilter.isEmpty()) {
|
||||||
|
logsetsAll.toList()
|
||||||
|
} else {
|
||||||
|
logsetsAll.filter { it.contains(currentFilter) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -472,7 +472,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_CONFIG_FINISH -> {
|
AppConfig.MSG_MEASURE_CONFIG_FINISH -> {
|
||||||
onTestsFinished()
|
val content = intent.getStringExtra("content")
|
||||||
|
if (content == "0") {
|
||||||
|
onTestsFinished()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package com.v2ray.ang.viewmodel
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.AssetUrlItem
|
||||||
|
import com.v2ray.ang.extension.concatUrl
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
|
||||||
|
class UserAssetViewModel : ViewModel() {
|
||||||
|
private val assets = mutableListOf<Pair<String, AssetUrlItem>>()
|
||||||
|
private val builtInGeoFiles = listOf("geosite.dat", "geoip.dat")
|
||||||
|
|
||||||
|
val itemCount: Int
|
||||||
|
get() = assets.size
|
||||||
|
|
||||||
|
fun getAssets(): List<Pair<String, AssetUrlItem>> = assets.toList()
|
||||||
|
|
||||||
|
fun getAsset(position: Int): Pair<String, AssetUrlItem>? = assets.getOrNull(position)
|
||||||
|
|
||||||
|
fun reload(geoFilesSource: String) {
|
||||||
|
val decoded = MmkvManager.decodeAssetUrls()
|
||||||
|
assets.clear()
|
||||||
|
assets.addAll(buildAssetList(decoded, geoFilesSource))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildAssetList(
|
||||||
|
decodedAssets: List<Pair<String, AssetUrlItem>>?,
|
||||||
|
geoFilesSource: String
|
||||||
|
): List<Pair<String, AssetUrlItem>> {
|
||||||
|
val savedAssets = decodedAssets ?: emptyList()
|
||||||
|
val builtInItems = builtInGeoFiles
|
||||||
|
.filter { geoFile -> savedAssets.none { it.second.remarks == geoFile } }
|
||||||
|
.map {
|
||||||
|
Utils.getUuid() to AssetUrlItem(
|
||||||
|
it,
|
||||||
|
String.format(AppConfig.GITHUB_DOWNLOAD_URL, geoFilesSource).concatUrl(it),
|
||||||
|
locked = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return builtInItems + savedAssets
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadGeoFiles(extDir: File, httpPort: Int): GeoDownloadResult {
|
||||||
|
val snapshot = getAssets()
|
||||||
|
var successCount = 0
|
||||||
|
val failures = mutableListOf<String>()
|
||||||
|
|
||||||
|
snapshot.forEach { (_, item) ->
|
||||||
|
val portsToTry = if (httpPort == 0) listOf(0) else listOf(httpPort, 0)
|
||||||
|
if (portsToTry.any { tryDownload(item, extDir, it) }) {
|
||||||
|
successCount++
|
||||||
|
} else {
|
||||||
|
failures.add(item.remarks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GeoDownloadResult(successCount, failures.size, failures)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryDownload(item: AssetUrlItem, extDir: File, httpPort: Int): Boolean {
|
||||||
|
val targetTemp = File(extDir, item.remarks + "_temp")
|
||||||
|
val target = File(extDir, item.remarks)
|
||||||
|
val conn = HttpUtil.createProxyConnection(item.url, httpPort, 15000, 15000, needStream = true) ?: return false
|
||||||
|
try {
|
||||||
|
val responseCode = conn.responseCode
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
|
conn.inputStream.use { inputStream ->
|
||||||
|
FileOutputStream(targetTemp).use { output ->
|
||||||
|
inputStream.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
targetTemp.renameTo(target)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
data class GeoDownloadResult(
|
||||||
|
val successCount: Int,
|
||||||
|
val failureCount: Int,
|
||||||
|
val failedAssets: List<String>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-1
Submodule hev-socks5-tunnel updated: 09aed689d7...da33382c72
Reference in New Issue
Block a user