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
|
||||
with:
|
||||
log-accepted-android-sdk-licenses: false
|
||||
cmdline-tools-version: '12266719'
|
||||
packages: 'platforms;android-35 build-tools;35.0.0 platform-tools'
|
||||
cmdline-tools-version: '13114758'
|
||||
packages: 'platforms;android-36.1 build-tools;36.1.0 platform-tools'
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
|
||||
+1
-1
Submodule AndroidLibXrayLite updated: 16222c3387...df33063839
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "com.v2ray.ang"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 701
|
||||
versionName = "2.0.1"
|
||||
versionCode = 705
|
||||
versionName = "2.0.5"
|
||||
multiDexEnabled = true
|
||||
|
||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||
|
||||
@@ -42,6 +42,13 @@
|
||||
"name": "xray0",
|
||||
"MTU": 1500,
|
||||
"userLevel": 8
|
||||
},
|
||||
"sniffing": {
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -4,6 +4,5 @@ data class ConfigResult(
|
||||
var status: Boolean,
|
||||
var guid: String? = null,
|
||||
var content: String = "",
|
||||
var socksPort: Int? = null,
|
||||
)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ data class V2rayConfig(
|
||||
var protocol: String,
|
||||
var listen: String? = null,
|
||||
var settings: InSettingsBean? = null,
|
||||
val sniffing: SniffingBean? = null,
|
||||
var sniffing: SniffingBean? = null,
|
||||
val streamSettings: Any? = null,
|
||||
val allocate: Any? = null
|
||||
) {
|
||||
@@ -88,6 +88,7 @@ data class V2rayConfig(
|
||||
var reserved: List<Int>? = null,
|
||||
var mtu: Int? = null,
|
||||
var obfsPassword: String? = null,
|
||||
var version: Int? = null,
|
||||
) {
|
||||
|
||||
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.
|
||||
*/
|
||||
fun CharSequence?.isNotNullEmpty(): Boolean = this != null && this.isNotEmpty()
|
||||
fun CharSequence?.isNotNullEmpty(): Boolean = !this.isNullOrBlank()
|
||||
|
||||
fun String.concatUrl(vararg paths: String): String {
|
||||
val builder = StringBuilder(this.trimEnd('/'))
|
||||
|
||||
@@ -26,7 +26,7 @@ object Hysteria2Fmt : FmtBase() {
|
||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
||||
|
||||
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.serverPort = uri.port.toString()
|
||||
config.password = uri.userInfo
|
||||
@@ -96,6 +96,7 @@ object Hysteria2Fmt : FmtBase() {
|
||||
outboundBean.settings?.let { server ->
|
||||
server.address = getServerAddress(profileItem)
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
server.version = 2
|
||||
}
|
||||
|
||||
val sni = outboundBean.streamSettings?.let {
|
||||
|
||||
@@ -36,7 +36,7 @@ object ShadowsocksFmt : FmtBase() {
|
||||
if (uri.port <= 0) 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.serverPort = uri.port.toString()
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ object SocksFmt : FmtBase() {
|
||||
if (uri.idnHost.isEmpty()) 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.serverPort = uri.port.toString()
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ object TrojanFmt : FmtBase() {
|
||||
val config = ProfileItem.create(EConfigType.TROJAN)
|
||||
|
||||
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.serverPort = uri.port.toString()
|
||||
config.password = uri.userInfo
|
||||
|
||||
@@ -26,7 +26,7 @@ object VlessFmt : FmtBase() {
|
||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||
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.serverPort = uri.port.toString()
|
||||
config.password = uri.userInfo
|
||||
|
||||
@@ -163,7 +163,7 @@ object VmessFmt : FmtBase() {
|
||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||
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.serverPort = uri.port.toString()
|
||||
config.password = uri.userInfo
|
||||
|
||||
@@ -25,7 +25,7 @@ object WireguardFmt : FmtBase() {
|
||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||
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.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.GEOSITE_PRIVATE
|
||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||
import com.v2ray.ang.AppConfig.VPN
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.Language
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
@@ -386,6 +387,15 @@ object SettingsManager {
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -108,7 +108,7 @@ object V2RayServiceManager {
|
||||
} else {
|
||||
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)
|
||||
} else {
|
||||
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.v2ray.ang.handler
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import com.google.gson.JsonArray
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.ConfigResult
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
@@ -44,7 +45,7 @@ object V2rayConfigManager {
|
||||
try {
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
|
||||
return if (config.configType == EConfigType.CUSTOM) {
|
||||
getV2rayCustomConfig(guid, config)
|
||||
getV2rayCustomConfig(context, guid, config)
|
||||
} else if (config.configType == EConfigType.POLICYGROUP) {
|
||||
getV2rayGroupConfig(context, guid, config)
|
||||
} else {
|
||||
@@ -67,7 +68,7 @@ object V2rayConfigManager {
|
||||
try {
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
|
||||
return if (config.configType == EConfigType.CUSTOM) {
|
||||
getV2rayCustomConfig(guid, config)
|
||||
getV2rayCustomConfig(context, guid, config)
|
||||
} else if (config.configType == EConfigType.POLICYGROUP) {
|
||||
// The number of policy groups will not be very large, so no special handling is needed.
|
||||
getV2rayGroupConfig(context, guid, config)
|
||||
@@ -87,9 +88,43 @@ object V2rayConfigManager {
|
||||
* @param config The profile item containing the configuration details.
|
||||
* @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)
|
||||
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? {
|
||||
var assets = ""
|
||||
if (SettingsManager.isUsingHevTun()) {
|
||||
assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json")
|
||||
if (TextUtils.isEmpty(assets)) {
|
||||
return null
|
||||
}
|
||||
initConfigCache = assets
|
||||
} else {
|
||||
if (needTun()) {
|
||||
assets = initConfigCacheWithTun ?: Utils.readTextFromAssets(context, "v2ray_config_with_tun.json")
|
||||
if (TextUtils.isEmpty(assets)) {
|
||||
return null
|
||||
}
|
||||
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)
|
||||
return config
|
||||
@@ -322,6 +357,10 @@ object V2rayConfigManager {
|
||||
|
||||
//region some sub function
|
||||
|
||||
private fun needTun(): Boolean {
|
||||
return SettingsManager.isVpnMode() && !SettingsManager.isUsingHevTun()
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the inbound settings for V2ray.
|
||||
*
|
||||
@@ -360,9 +399,10 @@ object V2rayConfigManager {
|
||||
v2rayConfig.inbounds.add(inbound2)
|
||||
}
|
||||
|
||||
if (!SettingsManager.isUsingHevTun()) {
|
||||
if (needTun()) {
|
||||
val inboundTun = v2rayConfig.inbounds.firstOrNull { e -> e.tag == "tun" }
|
||||
inboundTun?.settings?.mtu = SettingsManager.getVpnMtu()
|
||||
inboundTun?.sniffing = inbound1.sniffing
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Failed to configure inbounds", e)
|
||||
@@ -484,23 +524,25 @@ object V2rayConfigManager {
|
||||
)
|
||||
}
|
||||
|
||||
if (SettingsManager.isUsingHevTun()) {
|
||||
//hev-socks5-tunnel dns routing
|
||||
v2rayConfig.routing.rules.add(
|
||||
0, RulesBean(
|
||||
inboundTag = arrayListOf("socks"),
|
||||
outboundTag = "dns-out",
|
||||
port = "53",
|
||||
if(SettingsManager.isVpnMode()) {
|
||||
if (SettingsManager.isUsingHevTun()) {
|
||||
//hev-socks5-tunnel dns routing
|
||||
v2rayConfig.routing.rules.add(
|
||||
0, RulesBean(
|
||||
inboundTag = arrayListOf("socks"),
|
||||
outboundTag = "dns-out",
|
||||
port = "53",
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
v2rayConfig.routing.rules.add(
|
||||
0, RulesBean(
|
||||
inboundTag = arrayListOf("tun"),
|
||||
outboundTag = "dns-out",
|
||||
port = "53",
|
||||
} else {
|
||||
v2rayConfig.routing.rules.add(
|
||||
0, RulesBean(
|
||||
inboundTag = arrayListOf("tun"),
|
||||
outboundTag = "dns-out",
|
||||
port = "53",
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// DNS outbound
|
||||
@@ -1192,7 +1234,11 @@ object V2rayConfigManager {
|
||||
if (profileItem.portHopping.isNotNullEmpty()) {
|
||||
hysteriaSetting.udphop = StreamSettingsBean.HysteriaSettingsBean.HysteriaUdpHopBean(
|
||||
port = profileItem.portHopping,
|
||||
interval = profileItem.portHoppingInterval?.ifEmpty { "30" }.orEmpty().toInt()
|
||||
interval = profileItem.portHoppingInterval
|
||||
?.trim()
|
||||
?.toIntOrNull()
|
||||
?.takeIf { it >= 5 }
|
||||
?: 30
|
||||
)
|
||||
}
|
||||
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.MSG_MEASURE_CONFIG
|
||||
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.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.cancelChildren
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.Collections
|
||||
|
||||
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
|
||||
private val realTestRunningCount = AtomicInteger(0)
|
||||
private val realTestCount = AtomicInteger(0)
|
||||
// manage active batch workers so each batch is independent and cancellable
|
||||
private val activeWorkers = Collections.synchronizedList(mutableListOf<RealPingWorkerService>())
|
||||
|
||||
/**
|
||||
* Initializes the V2Ray environment.
|
||||
@@ -57,8 +38,10 @@ class V2RayTestService : Service() {
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
realTestJob.cancel()
|
||||
realDispatcher.close()
|
||||
// cancel any active workers
|
||||
val snapshot = ArrayList(activeWorkers)
|
||||
snapshot.forEach { it.cancel() }
|
||||
activeWorkers.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,64 +56,24 @@ class V2RayTestService : Service() {
|
||||
MSG_MEASURE_CONFIG -> {
|
||||
val guidsList = intent.serializable<ArrayList<String>>("content")
|
||||
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 -> {
|
||||
realTestJob.cancelChildren()
|
||||
// cancel all running batch workers independently
|
||||
val snapshot = ArrayList(activeWorkers)
|
||||
snapshot.forEach { it.cancel() }
|
||||
activeWorkers.clear()
|
||||
}
|
||||
}
|
||||
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) {
|
||||
override fun getItemCount(): Int = groups.size
|
||||
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")
|
||||
fun update(groups: List<GroupMapItem>) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -9,7 +8,6 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.TAG
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.FragmentGroupServerBinding
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
@@ -51,14 +49,14 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
||||
if (mainViewModel.subscriptionId != subId) {
|
||||
return@observe
|
||||
}
|
||||
Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
|
||||
// Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
|
||||
adapter.setData(mainViewModel.serversCache, index)
|
||||
}
|
||||
mainViewModel.isRunning.observe(viewLifecycleOwner) { isRunning ->
|
||||
adapter.isRunning = isRunning
|
||||
}
|
||||
|
||||
Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
|
||||
// Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
||||
@@ -2,37 +2,34 @@ package com.v2ray.ang.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.databinding.ActivityLogcatBinding
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.viewmodel.LogcatViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
||||
|
||||
private var logsetsAll: MutableList<String> = mutableListOf()
|
||||
var logsets: MutableList<String> = mutableListOf()
|
||||
private val adapter by lazy { LogcatRecyclerAdapter(this) }
|
||||
private val viewModel: LogcatViewModel by viewModels()
|
||||
private lateinit var adapter: LogcatRecyclerAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
//setContentView(binding.root)
|
||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_logcat))
|
||||
|
||||
adapter = LogcatRecyclerAdapter(viewModel, ::onLogLongClick)
|
||||
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||
@@ -40,58 +37,12 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||
|
||||
binding.refreshLayout.setOnRefreshListener(this)
|
||||
|
||||
logsets.add(getString(R.string.pull_down_to_refresh))
|
||||
toast(getString(R.string.pull_down_to_refresh))
|
||||
}
|
||||
|
||||
private fun getLogcat() {
|
||||
|
||||
try {
|
||||
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)
|
||||
}
|
||||
private fun onLogLongClick(log: String): Boolean {
|
||||
Utils.setClipboard(this, log)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
@@ -104,12 +55,14 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
filterLogs(newText)
|
||||
viewModel.filter(newText)
|
||||
refreshData()
|
||||
return false
|
||||
}
|
||||
})
|
||||
searchView.setOnCloseListener {
|
||||
filterLogs("")
|
||||
viewModel.filter("")
|
||||
refreshData()
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -119,33 +72,33 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
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)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.clear_all -> {
|
||||
clearLogcat()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
viewModel.clearLogcat()
|
||||
withContext(Dispatchers.Main) {
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
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() {
|
||||
getLogcat()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
viewModel.loadLogcat()
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
|
||||
@@ -6,17 +6,20 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.v2ray.ang.AppConfig
|
||||
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>() {
|
||||
private var mActivity: LogcatActivity = activity
|
||||
class LogcatRecyclerAdapter(
|
||||
private val viewModel: LogcatViewModel,
|
||||
private val onLongClick: ((String) -> Boolean)? = null
|
||||
) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() {
|
||||
|
||||
|
||||
override fun getItemCount() = mActivity.logsets.size
|
||||
override fun getItemCount() = viewModel.getAll().size
|
||||
|
||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||
try {
|
||||
val log = mActivity.logsets[position]
|
||||
val logs = viewModel.getAll()
|
||||
val log = logs[position]
|
||||
|
||||
if (log.isEmpty()) {
|
||||
holder.itemSubSettingBinding.logTag.text = ""
|
||||
holder.itemSubSettingBinding.logContent.text = ""
|
||||
@@ -27,8 +30,7 @@ class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter
|
||||
}
|
||||
|
||||
holder.itemView.setOnLongClickListener {
|
||||
Utils.setClipboard(mActivity, log)
|
||||
true
|
||||
onLongClick?.invoke(log) ?: false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
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.tabs.TabLayoutMediator
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.VPN
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityMainBinding
|
||||
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.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsChangeManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.handler.V2RayServiceManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.viewmodel.MainViewModel
|
||||
@@ -115,52 +115,20 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(binding.root)
|
||||
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)
|
||||
}
|
||||
}
|
||||
setupToolbar(binding.toolbar, false, getString(R.string.title_server))
|
||||
|
||||
// setup viewpager and tablayout
|
||||
groupPagerAdapter = GroupPagerAdapter(this, emptyList())
|
||||
binding.viewPager.adapter = groupPagerAdapter
|
||||
binding.viewPager.isUserInputEnabled = true
|
||||
|
||||
// setup navigation drawer
|
||||
val toggle = ActionBarDrawerToggle(
|
||||
this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
|
||||
)
|
||||
binding.drawerLayout.addDrawerListener(toggle)
|
||||
toggle.syncState()
|
||||
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) {
|
||||
override fun handleOnBackPressed() {
|
||||
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()
|
||||
|
||||
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() {
|
||||
mainViewModel.updateTestResultAction.observe(this) { setTestState(it) }
|
||||
mainViewModel.isRunning.observe(this) { isRunning ->
|
||||
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
|
||||
}
|
||||
applyRunningState(false, isRunning)
|
||||
}
|
||||
mainViewModel.startListenBroadcast()
|
||||
mainViewModel.initAssets(assets)
|
||||
@@ -214,6 +183,32 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
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() {
|
||||
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
||||
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() {
|
||||
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 {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) {
|
||||
moveTaskToBack(false)
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Collections
|
||||
|
||||
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
||||
companion object {
|
||||
@@ -282,6 +283,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
private fun removeServer(guid: String, position: Int) {
|
||||
if (guid == MmkvManager.getSelectServer()) {
|
||||
application.toast(R.string.toast_action_not_allowed)
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
mActivity.mainViewModel.swapServer(fromPosition, toPosition)
|
||||
if (fromPosition < data.size && toPosition < data.size) {
|
||||
Collections.swap(data, fromPosition, toPosition)
|
||||
}
|
||||
notifyItemMoved(fromPosition, toPosition)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
class RoutingSettingActivity : BaseActivity() {
|
||||
private val binding by lazy { ActivityRoutingSettingBinding.inflate(layoutInflater) }
|
||||
|
||||
private val ownerActivity: RoutingSettingActivity
|
||||
get() = this
|
||||
private val viewModel: RoutingSettingsViewModel by viewModels()
|
||||
private lateinit var adapter: RoutingSettingRecyclerAdapter
|
||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||
@@ -57,7 +58,7 @@ class RoutingSettingActivity : BaseActivity() {
|
||||
//setContentView(binding.root)
|
||||
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.layoutManager = LinearLayoutManager(this)
|
||||
@@ -202,4 +203,23 @@ class RoutingSettingActivity : BaseActivity() {
|
||||
viewModel.reload()
|
||||
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
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -13,12 +12,11 @@ import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||
import com.v2ray.ang.viewmodel.RoutingSettingsViewModel
|
||||
|
||||
class RoutingSettingRecyclerAdapter(
|
||||
val activity: RoutingSettingActivity,
|
||||
private val viewModel: RoutingSettingsViewModel
|
||||
private val viewModel: RoutingSettingsViewModel,
|
||||
private val adapterListener: BaseAdapterListener?
|
||||
) : RecyclerView.Adapter<RoutingSettingRecyclerAdapter.MainViewHolder>(),
|
||||
ItemTouchHelperAdapter {
|
||||
|
||||
private var mActivity: RoutingSettingActivity = activity
|
||||
override fun getItemCount() = viewModel.getAll().size
|
||||
|
||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||
@@ -33,10 +31,7 @@ class RoutingSettingRecyclerAdapter(
|
||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||
|
||||
holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener {
|
||||
mActivity.startActivity(
|
||||
Intent(mActivity, RoutingEditActivity::class.java)
|
||||
.putExtra("position", position)
|
||||
)
|
||||
adapterListener?.onEdit("", position)
|
||||
}
|
||||
|
||||
holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
||||
@@ -76,7 +71,7 @@ class RoutingSettingRecyclerAdapter(
|
||||
}
|
||||
|
||||
override fun onItemMoveCompleted() {
|
||||
mActivity.refreshData()
|
||||
adapterListener?.onRefreshData()
|
||||
}
|
||||
|
||||
override fun onItemDismiss(position: Int) {
|
||||
|
||||
@@ -21,23 +21,18 @@ import com.v2ray.ang.util.Utils
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SettingsActivity : BaseActivity() {
|
||||
//private val settingsViewModel: SettingsViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
//setContentView(R.layout.activity_settings)
|
||||
setContentViewWithToolbar(R.layout.activity_settings, showHomeAsUp = true, title = getString(R.string.title_settings))
|
||||
|
||||
//settingsViewModel.startListenPreferenceChange()
|
||||
}
|
||||
|
||||
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 fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
|
||||
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 vpnBypassLan by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_BYPASS_LAN) }
|
||||
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 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 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 useTun by lazy { findPreference<ListPreference>(AppConfig.PREF_TUN) }
|
||||
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 useHevTun by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_USE_HEV_TUNNEL) }
|
||||
|
||||
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
|
||||
// Use MMKV as the storage backend for all Preferences
|
||||
@@ -77,30 +65,10 @@ class SettingsActivity : BaseActivity() {
|
||||
|
||||
initPreferenceSummaries()
|
||||
|
||||
// perAppProxy?.setOnPreferenceClickListener {
|
||||
// startActivity(Intent(activity, PerAppProxyActivity::class.java))
|
||||
// perAppProxy?.isChecked = true
|
||||
// false
|
||||
// }
|
||||
localDns?.setOnPreferenceChangeListener { _, any ->
|
||||
updateLocalDns(any as Boolean)
|
||||
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 ->
|
||||
updateMux(newValue as Boolean)
|
||||
@@ -119,18 +87,6 @@ class SettingsActivity : BaseActivity() {
|
||||
updateFragment(newValue as Boolean)
|
||||
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 ->
|
||||
val value = newValue as Boolean
|
||||
@@ -141,65 +97,21 @@ class SettingsActivity : BaseActivity() {
|
||||
}
|
||||
true
|
||||
}
|
||||
// autoUpdateInterval?.setOnPreferenceChangeListener { _, any ->
|
||||
// var nval = any as String
|
||||
//
|
||||
// // It must be greater than 15 minutes because WorkManager couldn't run tasks under 15 minutes intervals
|
||||
// nval =
|
||||
// if (TextUtils.isEmpty(nval) || nval.toLongEx() < 15) AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL else nval
|
||||
// autoUpdateInterval?.summary = nval
|
||||
// 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())
|
||||
mode?.setOnPreferenceChangeListener { pref, newValue ->
|
||||
val valueStr = newValue.toString()
|
||||
(pref as? ListPreference)?.let { lp ->
|
||||
val idx = lp.findIndexOfValue(valueStr)
|
||||
lp.summary = if (idx >= 0) lp.entries[idx] else valueStr
|
||||
}
|
||||
updateMode(valueStr)
|
||||
true
|
||||
}
|
||||
mode?.dialogLayoutResource = R.layout.preference_with_help_link
|
||||
//loglevel.summary = "LogLevel"
|
||||
|
||||
// useTun?.setOnPreferenceChangeListener { _, newValue ->
|
||||
// updateHevTunSettings(newValue as String == AppConfig.TUN_hevsocks5)
|
||||
// true
|
||||
// }
|
||||
|
||||
// hevTunRwTimeout?.setOnPreferenceChangeListener { _, any ->
|
||||
// val nval = any as String
|
||||
// hevTunRwTimeout?.summary = nval.ifEmpty { AppConfig.HEVTUN_RW_TIMEOUT }
|
||||
// true
|
||||
// }
|
||||
useHevTun?.setOnPreferenceChangeListener { _, newValue ->
|
||||
updateHevTunSettings(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun initPreferenceSummaries() {
|
||||
@@ -212,6 +124,7 @@ class SettingsActivity : BaseActivity() {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
is ListPreference -> {
|
||||
pref.summary = pref.entry ?: ""
|
||||
pref.setOnPreferenceChangeListener { p, newValue ->
|
||||
@@ -221,6 +134,7 @@ class SettingsActivity : BaseActivity() {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
is CheckBoxPreference, is androidx.preference.SwitchPreferenceCompat -> {
|
||||
}
|
||||
}
|
||||
@@ -240,6 +154,8 @@ class SettingsActivity : BaseActivity() {
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
updateHevTunSettings(MmkvManager.decodeSettingsBool(AppConfig.PREF_USE_HEV_TUNNEL, true))
|
||||
|
||||
// Initialize mode-dependent UI states
|
||||
updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
|
||||
|
||||
@@ -251,101 +167,10 @@ class SettingsActivity : BaseActivity() {
|
||||
|
||||
// Initialize auto-update interval state
|
||||
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() {
|
||||
// listOf(
|
||||
// //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)
|
||||
private fun updateMode(value: String?) {
|
||||
val vpn = value == VPN
|
||||
localDns?.isEnabled = vpn
|
||||
fakeDns?.isEnabled = vpn
|
||||
appendHttpProxy?.isEnabled = vpn
|
||||
@@ -354,6 +179,8 @@ class SettingsActivity : BaseActivity() {
|
||||
vpnBypassLan?.isEnabled = vpn
|
||||
vpnInterfaceAddress?.isEnabled = vpn
|
||||
vpnMtu?.isEnabled = vpn
|
||||
useHevTun?.isEnabled = vpn
|
||||
updateHevTunSettings(false)
|
||||
if (vpn) {
|
||||
updateLocalDns(
|
||||
MmkvManager.decodeSettingsBool(
|
||||
@@ -361,6 +188,12 @@ class SettingsActivity : BaseActivity() {
|
||||
false
|
||||
)
|
||||
)
|
||||
updateHevTunSettings(
|
||||
MmkvManager.decodeSettingsBool(
|
||||
AppConfig.PREF_USE_HEV_TUNNEL,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,29 +256,12 @@ class SettingsActivity : BaseActivity() {
|
||||
fragmentPackets?.isEnabled = enabled
|
||||
fragmentLength?.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?) {
|
||||
// fragmentPackets?.summary = value.toString()
|
||||
// }
|
||||
//
|
||||
// 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
|
||||
// }
|
||||
|
||||
private fun updateHevTunSettings(enabled: Boolean) {
|
||||
hevTunLogLevel?.isEnabled = enabled
|
||||
hevTunRwTimeout?.isEnabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
fun onModeHelpClicked(view: View) {
|
||||
|
||||
@@ -3,18 +3,27 @@ package com.v2ray.ang.ui
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
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.toastSuccess
|
||||
import com.v2ray.ang.handler.AngConfigManager
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -22,17 +31,21 @@ import kotlinx.coroutines.launch
|
||||
|
||||
class SubSettingActivity : BaseActivity() {
|
||||
private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
|
||||
|
||||
private val ownerActivity: SubSettingActivity
|
||||
get() = this
|
||||
private val viewModel: SubscriptionsViewModel by viewModels()
|
||||
private lateinit var adapter: SubSettingRecyclerAdapter
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
//setContentView(binding.root)
|
||||
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.layoutManager = LinearLayoutManager(this)
|
||||
@@ -88,4 +101,62 @@ class SubSettingActivity : BaseActivity() {
|
||||
viewModel.reload()
|
||||
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
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
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.extension.toast
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||
import com.v2ray.ang.util.QRCodeDecoder
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.viewmodel.SubscriptionsViewModel
|
||||
|
||||
class SubSettingRecyclerAdapter(
|
||||
val activity: SubSettingActivity,
|
||||
private val viewModel: SubscriptionsViewModel
|
||||
private val viewModel: SubscriptionsViewModel,
|
||||
private val adapterListener: BaseAdapterListener?
|
||||
) : 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 onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||
@@ -45,14 +30,11 @@ class SubSettingRecyclerAdapter(
|
||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||
|
||||
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
||||
mActivity.startActivity(
|
||||
Intent(mActivity, SubEditActivity::class.java)
|
||||
.putExtra("subId", subId)
|
||||
)
|
||||
adapterListener?.onEdit(subId, position)
|
||||
}
|
||||
|
||||
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
||||
removeSubscription(subId, position)
|
||||
adapterListener?.onRemove(subId, position)
|
||||
}
|
||||
|
||||
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
||||
@@ -72,58 +54,11 @@ class SubSettingRecyclerAdapter(
|
||||
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
|
||||
holder.itemSubSettingBinding.layoutLastUpdated.visibility = View.VISIBLE
|
||||
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
||||
AlertDialog.Builder(mActivity)
|
||||
.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()
|
||||
adapterListener?.onShare(subItem.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return MainViewHolder(
|
||||
ItemRecyclerSubSettingBinding.inflate(
|
||||
@@ -154,7 +89,7 @@ class SubSettingRecyclerAdapter(
|
||||
}
|
||||
|
||||
override fun onItemMoveCompleted() {
|
||||
mActivity.refreshData()
|
||||
adapterListener?.onRefreshData()
|
||||
}
|
||||
|
||||
override fun onItemDismiss(position: Int) {
|
||||
|
||||
@@ -9,44 +9,36 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
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.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityUserAssetBinding
|
||||
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
||||
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.toastError
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.viewmodel.UserAssetViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
class UserAssetActivity : BaseActivity() {
|
||||
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 builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
|
||||
|
||||
private val requestStoragePermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
@@ -83,13 +75,13 @@ class UserAssetActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
//setContentView(binding.root)
|
||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_user_asset_setting))
|
||||
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
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.layoutGeoFilesSources.setOnClickListener {
|
||||
@@ -219,81 +211,25 @@ class UserAssetActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun downloadGeoFiles() {
|
||||
refreshData()
|
||||
showLoading()
|
||||
toast(R.string.msg_downloading_content)
|
||||
|
||||
val httpPort = SettingsManager.getHttpPort()
|
||||
var assets = MmkvManager.decodeAssetUrls()
|
||||
assets = addBuiltInGeoItems(assets)
|
||||
|
||||
var resultCount = 0
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
assets.forEach {
|
||||
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)
|
||||
}
|
||||
}
|
||||
val result = viewModel.downloadGeoFiles(extDir, httpPort)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (resultCount > 0) {
|
||||
toast(getString(R.string.title_update_config_count, resultCount))
|
||||
refreshData()
|
||||
if (result.successCount > 0) {
|
||||
toast(getString(R.string.title_update_config_count, result.successCount))
|
||||
} else {
|
||||
toast(getString(R.string.toast_failure))
|
||||
}
|
||||
refreshData()
|
||||
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() {
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
SettingsManager.initAssets(this@UserAssetActivity, assets)
|
||||
@@ -305,72 +241,41 @@ class UserAssetActivity : BaseActivity() {
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun refreshData() {
|
||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
||||
viewModel.reload(getGeoFilesSources())
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
||||
return UserAssetViewHolder(
|
||||
ItemRecyclerUserAssetBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
private inner class ActivityAdapterListener : BaseAdapterListener {
|
||||
override fun onEdit(guid: String, position: Int) {
|
||||
startActivity(
|
||||
Intent(ownerActivity, UserAssetUrlActivity::class.java)
|
||||
.putExtra("assetId", guid)
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
|
||||
var assets = MmkvManager.decodeAssetUrls()
|
||||
assets = addBuiltInGeoItems(assets)
|
||||
val item = assets.getOrNull(position) ?: return
|
||||
// file with name == item.second.remarks
|
||||
val file = extDir.listFiles()?.find { it.name == item.second.remarks }
|
||||
override fun onRemove(guid: String, position: Int) {
|
||||
val asset = viewModel.getAsset(position)?.takeIf { it.first == guid }
|
||||
?: viewModel.getAssets().find { it.first == guid }
|
||||
?: return
|
||||
val file = extDir.listFiles()?.find { it.name == asset.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 = getString(R.string.msg_file_not_found)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
AlertDialog.Builder(ownerActivity).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
file?.delete()
|
||||
MmkvManager.removeAssetUrl(guid)
|
||||
initAssets()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
// do nothing
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
var assets = MmkvManager.decodeAssetUrls()
|
||||
assets = addBuiltInGeoItems(assets)
|
||||
return assets.size
|
||||
override fun onShare(url: String) {
|
||||
}
|
||||
|
||||
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 -> {
|
||||
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