Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66f100ebb3 | |||
| b0213ffdf6 | |||
| 538b2eb0f8 | |||
| f2093c4c52 | |||
| 02ac57a4c6 | |||
| 379266f205 | |||
| 23dbb35da7 | |||
| 22a605ecda | |||
| c7dd45f4f2 | |||
| 609e688d87 | |||
| a2a4ea79a9 | |||
| c08147c362 | |||
| c99fda9839 | |||
| 587d103518 | |||
| 192a4afe6d | |||
| ee69364f2b | |||
| 6be0bd7b21 |
@@ -77,36 +77,6 @@ jobs:
|
|||||||
fileName: 'libv2ray.aar'
|
fileName: 'libv2ray.aar'
|
||||||
out-file-path: V2rayNG/app/libs/
|
out-file-path: V2rayNG/app/libs/
|
||||||
|
|
||||||
- name: Restore cached libhysteria2
|
|
||||||
id: cache-libhysteria2-restore
|
|
||||||
uses: actions/cache/restore@v4
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/hysteria/libs
|
|
||||||
key: libhysteria2-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }}
|
|
||||||
|
|
||||||
- name: Setup Golang
|
|
||||||
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
|
||||||
uses: actions/setup-go@v5.4.0
|
|
||||||
with:
|
|
||||||
go-version-file: 'AndroidLibXrayLite/go.mod'
|
|
||||||
cache: false
|
|
||||||
|
|
||||||
- name: Build libhysteria2
|
|
||||||
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
bash libhysteria2.sh
|
|
||||||
|
|
||||||
- name: Save libhysteria2
|
|
||||||
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
|
||||||
uses: actions/cache/save@v4
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/hysteria/libs
|
|
||||||
key: libhysteria2-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }}
|
|
||||||
|
|
||||||
- name: Copy libhysteria2
|
|
||||||
run: |
|
|
||||||
cp -r ${{ github.workspace }}/hysteria/libs ${{ github.workspace }}/V2rayNG/app
|
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4.7.0
|
uses: actions/setup-java@v4.7.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
[submodule "hysteria"]
|
|
||||||
path = hysteria
|
|
||||||
url = https://github.com/apernet/hysteria
|
|
||||||
[submodule "AndroidLibXrayLite"]
|
[submodule "AndroidLibXrayLite"]
|
||||||
path = AndroidLibXrayLite
|
path = AndroidLibXrayLite
|
||||||
url = https://github.com/2dust/AndroidLibXrayLite
|
url = https://github.com/2dust/AndroidLibXrayLite
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 700
|
versionCode = 702
|
||||||
versionName = "2.0.0"
|
versionName = "2.0.2"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"remarks": "Bypass bittorrent",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"protocol": [
|
||||||
|
"bittorrent"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Block udp443",
|
||||||
|
"outboundTag": "block",
|
||||||
|
"port": "443",
|
||||||
|
"network": "udp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Direct LAN IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Direct LAN domains",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"geosite:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Bypass Russia domains",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"geosite:category-ru"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Bypass Russia IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:ru"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -9,7 +9,9 @@ object AppConfig {
|
|||||||
|
|
||||||
/** Directory names used in the app's file system. */
|
/** Directory names used in the app's file system. */
|
||||||
const val DIR_ASSETS = "assets"
|
const val DIR_ASSETS = "assets"
|
||||||
const val DIR_BACKUPS = "backups"
|
|
||||||
|
const val WEBDAV_BACKUP_DIR = "backups"
|
||||||
|
const val WEBDAV_BACKUP_FILE_NAME = "backup_ng.zip"
|
||||||
|
|
||||||
/** Legacy configuration keys. */
|
/** Legacy configuration keys. */
|
||||||
const val ANG_CONFIG = "ang_config"
|
const val ANG_CONFIG = "ang_config"
|
||||||
@@ -169,6 +171,7 @@ object AppConfig {
|
|||||||
const val TROJAN = "trojan://"
|
const val TROJAN = "trojan://"
|
||||||
const val WIREGUARD = "wireguard://"
|
const val WIREGUARD = "wireguard://"
|
||||||
const val TUIC = "tuic://"
|
const val TUIC = "tuic://"
|
||||||
|
const val HYSTERIA = "hysteria://"
|
||||||
const val HYSTERIA2 = "hysteria2://"
|
const val HYSTERIA2 = "hysteria2://"
|
||||||
const val HY2 = "hy2://"
|
const val HY2 = "hy2://"
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ enum class EConfigType(val value: Int, val protocolScheme: String) {
|
|||||||
|
|
||||||
// TUIC(8, AppConfig.TUIC),
|
// TUIC(8, AppConfig.TUIC),
|
||||||
HYSTERIA2(9, AppConfig.HYSTERIA2),
|
HYSTERIA2(9, AppConfig.HYSTERIA2),
|
||||||
|
HYSTERIA(900, AppConfig.HYSTERIA),
|
||||||
HTTP(10, AppConfig.HTTP),
|
HTTP(10, AppConfig.HTTP),
|
||||||
POLICYGROUP (101, AppConfig.CUSTOM);
|
POLICYGROUP (101, AppConfig.CUSTOM);
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class Hysteria2Bean(
|
|
||||||
val server: String?,
|
|
||||||
val auth: String?,
|
|
||||||
val lazy: Boolean? = true,
|
|
||||||
val obfs: ObfsBean? = null,
|
|
||||||
val socks5: Socks5Bean? = null,
|
|
||||||
val http: Socks5Bean? = null,
|
|
||||||
val tls: TlsBean? = null,
|
|
||||||
val transport: TransportBean? = null,
|
|
||||||
val bandwidth: BandwidthBean? = null,
|
|
||||||
) {
|
|
||||||
data class ObfsBean(
|
|
||||||
val type: String?,
|
|
||||||
val salamander: SalamanderBean?
|
|
||||||
) {
|
|
||||||
data class SalamanderBean(
|
|
||||||
val password: String?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Socks5Bean(
|
|
||||||
val listen: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TlsBean(
|
|
||||||
val sni: String?,
|
|
||||||
val insecure: Boolean?,
|
|
||||||
val pinSHA256: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TransportBean(
|
|
||||||
val type: String?,
|
|
||||||
val udp: TransportUdpBean?
|
|
||||||
) {
|
|
||||||
data class TransportUdpBean(
|
|
||||||
val hopInterval: String?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class BandwidthBean(
|
|
||||||
val down: String?,
|
|
||||||
val up: String?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,8 @@ enum class NetworkType(val type: String) {
|
|||||||
H2("h2"),
|
H2("h2"),
|
||||||
|
|
||||||
//QUIC("quic"),
|
//QUIC("quic"),
|
||||||
GRPC("grpc");
|
GRPC("grpc"),
|
||||||
|
HYSTERIA("hysteria");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromString(type: String?) = entries.find { it.type == type } ?: TCP
|
fun fromString(type: String?) = entries.find { it.type == type } ?: TCP
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ enum class RoutingType(val fileName: String) {
|
|||||||
WHITE("custom_routing_white"),
|
WHITE("custom_routing_white"),
|
||||||
BLACK("custom_routing_black"),
|
BLACK("custom_routing_black"),
|
||||||
GLOBAL("custom_routing_global"),
|
GLOBAL("custom_routing_global"),
|
||||||
WHITE_IRAN("custom_routing_white_iran");
|
WHITE_IRAN("custom_routing_white_iran"),
|
||||||
|
WHITE_RUSSIA("custom_routing_white_russia");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromIndex(index: Int): RoutingType {
|
fun fromIndex(index: Int): RoutingType {
|
||||||
@@ -13,6 +14,7 @@ enum class RoutingType(val fileName: String) {
|
|||||||
1 -> BLACK
|
1 -> BLACK
|
||||||
2 -> GLOBAL
|
2 -> GLOBAL
|
||||||
3 -> WHITE_IRAN
|
3 -> WHITE_IRAN
|
||||||
|
4 -> WHITE_RUSSIA
|
||||||
else -> WHITE
|
else -> WHITE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.v2ray.ang.dto
|
|||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
data class V2rayConfig(
|
data class V2rayConfig(
|
||||||
var remarks: String? = null,
|
var remarks: String? = null,
|
||||||
@@ -76,7 +75,7 @@ data class V2rayConfig(
|
|||||||
/*DNS*/
|
/*DNS*/
|
||||||
val network: String? = null,
|
val network: String? = null,
|
||||||
var address: Any? = null,
|
var address: Any? = null,
|
||||||
val port: Int? = null,
|
var port: Int? = null,
|
||||||
/*Freedom*/
|
/*Freedom*/
|
||||||
var domainStrategy: String? = null,
|
var domainStrategy: String? = null,
|
||||||
val redirect: String? = null,
|
val redirect: String? = null,
|
||||||
@@ -160,7 +159,8 @@ data class V2rayConfig(
|
|||||||
var quicSettings: QuicSettingBean? = null,
|
var quicSettings: QuicSettingBean? = null,
|
||||||
var realitySettings: TlsSettingsBean? = null,
|
var realitySettings: TlsSettingsBean? = null,
|
||||||
var grpcSettings: GrpcSettingsBean? = null,
|
var grpcSettings: GrpcSettingsBean? = null,
|
||||||
var hy2steriaSettings: Hy2steriaSettingsBean? = null,
|
var hysteriaSettings: HysteriaSettingsBean? = null,
|
||||||
|
var udpmasks: List<UdpMasksBean>? = null,
|
||||||
val dsSettings: Any? = null,
|
val dsSettings: Any? = null,
|
||||||
var sockopt: SockoptBean? = null
|
var sockopt: SockoptBean? = null
|
||||||
) {
|
) {
|
||||||
@@ -247,7 +247,8 @@ data class V2rayConfig(
|
|||||||
var dialerProxy: String? = null,
|
var dialerProxy: String? = null,
|
||||||
var domainStrategy: String? = null,
|
var domainStrategy: String? = null,
|
||||||
var happyEyeballs: HappyEyeballsBean? = null,
|
var happyEyeballs: HappyEyeballsBean? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class HappyEyeballsBean(
|
data class HappyEyeballsBean(
|
||||||
var prioritizeIPv6: Boolean? = null,
|
var prioritizeIPv6: Boolean? = null,
|
||||||
var maxConcurrentTry: Int? = 4,
|
var maxConcurrentTry: Int? = 4,
|
||||||
@@ -293,18 +294,27 @@ data class V2rayConfig(
|
|||||||
var health_check_timeout: Int? = null
|
var health_check_timeout: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Hy2steriaSettingsBean(
|
data class HysteriaSettingsBean(
|
||||||
var password: String? = null,
|
var version: Int,
|
||||||
var use_udp_extension: Boolean? = true,
|
var auth: String? = null,
|
||||||
var congestion: Hy2CongestionBean? = null
|
var up: String? = null,
|
||||||
|
var down: String? = null,
|
||||||
|
var udphop: HysteriaUdpHopBean? = null
|
||||||
) {
|
) {
|
||||||
data class Hy2CongestionBean(
|
data class HysteriaUdpHopBean(
|
||||||
var type: String? = "bbr",
|
var port: String? = null,
|
||||||
var up_mbps: Int? = null,
|
var interval: Int? = null
|
||||||
var down_mbps: Int? = null,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class UdpMasksBean(
|
||||||
|
var type: String,
|
||||||
|
var settings: UdpMasksSettingsBean? = null
|
||||||
|
) {
|
||||||
|
data class UdpMasksSettingsBean(
|
||||||
|
var password: String? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MuxBean(
|
data class MuxBean(
|
||||||
@@ -323,11 +333,14 @@ data class V2rayConfig(
|
|||||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|
||||||
) {
|
) {
|
||||||
return settings?.servers?.first()?.address
|
return settings?.servers?.first()?.address
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||||
return settings?.peers?.first()?.endpoint?.substringBeforeLast(":")
|
return settings?.peers?.first()?.endpoint?.substringBeforeLast(":")
|
||||||
|
} else if (protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HYSTERIA.name, true)
|
||||||
|
) {
|
||||||
|
return settings?.address as String?
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -341,132 +354,14 @@ data class V2rayConfig(
|
|||||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|
||||||
) {
|
) {
|
||||||
return settings?.servers?.first()?.port
|
return settings?.servers?.first()?.port
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||||
return settings?.peers?.first()?.endpoint?.substringAfterLast(":")?.toInt()
|
return settings?.peers?.first()?.endpoint?.substringAfterLast(":")?.toInt()
|
||||||
}
|
} else if (protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
return null
|
|| protocol.equals(EConfigType.HYSTERIA.name, true)
|
||||||
}
|
|
||||||
|
|
||||||
fun getServerAddressAndPort(): String {
|
|
||||||
val address = getServerAddress().orEmpty()
|
|
||||||
val port = getServerPort()
|
|
||||||
return Utils.getIpv6Address(address) + ":" + port
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPassword(): String? {
|
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|
||||||
) {
|
) {
|
||||||
return settings?.vnext?.first()?.users?.first()?.id
|
return settings?.port
|
||||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.servers?.first()?.password
|
|
||||||
} else if (protocol.equals(EConfigType.SOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.servers?.first()?.users?.first()?.pass
|
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
|
||||||
return settings?.secretKey
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSecurityEncryption(): String? {
|
|
||||||
return when {
|
|
||||||
protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.first()?.users?.first()?.security
|
|
||||||
protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.first()?.users?.first()?.encryption
|
|
||||||
protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.first()?.method
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTransportSettingDetails(): List<String?>? {
|
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|
||||||
|| protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
|
||||||
) {
|
|
||||||
val transport = streamSettings?.network ?: return null
|
|
||||||
return when (transport) {
|
|
||||||
NetworkType.TCP.type -> {
|
|
||||||
val tcpSetting = streamSettings?.tcpSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
tcpSetting.header.type,
|
|
||||||
tcpSetting.header.request?.headers?.Host?.joinToString(",").orEmpty(),
|
|
||||||
tcpSetting.header.request?.path?.joinToString(",").orEmpty()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.KCP.type -> {
|
|
||||||
val kcpSetting = streamSettings?.kcpSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
kcpSetting.header.type,
|
|
||||||
"",
|
|
||||||
kcpSetting.seed.orEmpty()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.WS.type -> {
|
|
||||||
val wsSetting = streamSettings?.wsSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
"",
|
|
||||||
wsSetting.headers.Host,
|
|
||||||
wsSetting.path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.HTTP_UPGRADE.type -> {
|
|
||||||
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
"",
|
|
||||||
httpupgradeSetting.host,
|
|
||||||
httpupgradeSetting.path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.XHTTP.type -> {
|
|
||||||
val xhttpSettings = streamSettings?.xhttpSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
"",
|
|
||||||
xhttpSettings.host,
|
|
||||||
xhttpSettings.path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.H2.type -> {
|
|
||||||
val h2Setting = streamSettings?.httpSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
"",
|
|
||||||
h2Setting.host.joinToString(","),
|
|
||||||
h2Setting.path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// "quic" -> {
|
|
||||||
// val quicSetting = streamSettings?.quicSettings ?: return null
|
|
||||||
// listOf(
|
|
||||||
// quicSetting.header.type,
|
|
||||||
// quicSetting.security,
|
|
||||||
// quicSetting.key
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
NetworkType.GRPC.type -> {
|
|
||||||
val grpcSetting = streamSettings?.grpcSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
if (grpcSetting.multiMode == true) "multi" else "gun",
|
|
||||||
grpcSetting.authority.orEmpty(),
|
|
||||||
grpcSetting.serviceName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('/'))
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ open class FmtBase {
|
|||||||
config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() }
|
config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() }
|
||||||
config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() }
|
config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dicQuery
|
return dicQuery
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package com.v2ray.ang.fmt
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.Hysteria2Bean
|
import com.v2ray.ang.dto.NetworkType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean.UdpMasksBean.UdpMasksSettingsBean
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
@@ -30,6 +31,7 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
config.security = AppConfig.TLS
|
config.security = AppConfig.TLS
|
||||||
|
config.network = NetworkType.HYSTERIA.type
|
||||||
|
|
||||||
if (!uri.rawQuery.isNullOrEmpty()) {
|
if (!uri.rawQuery.isNullOrEmpty()) {
|
||||||
val queryParam = getQueryParam(uri)
|
val queryParam = getQueryParam(uri)
|
||||||
@@ -80,64 +82,6 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
return toUri(config, config.password, dicQuery)
|
return toUri(config, config.password, dicQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to a Hysteria2Bean object.
|
|
||||||
*
|
|
||||||
* @param config the ProfileItem object to convert
|
|
||||||
* @param socksPort the port number for the socks5 proxy
|
|
||||||
* @return the converted Hysteria2Bean object, or null if conversion fails
|
|
||||||
*/
|
|
||||||
fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? {
|
|
||||||
|
|
||||||
val obfs = if (config.obfsPassword.isNullOrEmpty()) null else
|
|
||||||
Hysteria2Bean.ObfsBean(
|
|
||||||
type = "salamander",
|
|
||||||
salamander = Hysteria2Bean.ObfsBean.SalamanderBean(
|
|
||||||
password = config.obfsPassword
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val transport = if (config.portHopping.isNullOrEmpty()) null else
|
|
||||||
Hysteria2Bean.TransportBean(
|
|
||||||
type = "udp",
|
|
||||||
udp = Hysteria2Bean.TransportBean.TransportUdpBean(
|
|
||||||
hopInterval = (config.portHoppingInterval?.takeIf { it.isNotEmpty() } ?: "30") + "s"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val bandwidth = if (config.bandwidthDown.isNullOrEmpty() || config.bandwidthUp.isNullOrEmpty()) null else
|
|
||||||
Hysteria2Bean.BandwidthBean(
|
|
||||||
down = config.bandwidthDown,
|
|
||||||
up = config.bandwidthUp,
|
|
||||||
)
|
|
||||||
|
|
||||||
val server =
|
|
||||||
if (config.portHopping.isNullOrEmpty())
|
|
||||||
config.getServerAddressAndPort()
|
|
||||||
else
|
|
||||||
Utils.getIpv6Address(config.server) + ":" + config.portHopping
|
|
||||||
|
|
||||||
val bean = Hysteria2Bean(
|
|
||||||
server = server,
|
|
||||||
auth = config.password,
|
|
||||||
obfs = obfs,
|
|
||||||
transport = transport,
|
|
||||||
bandwidth = bandwidth,
|
|
||||||
socks5 = Hysteria2Bean.Socks5Bean(
|
|
||||||
listen = "$LOOPBACK:${socksPort}",
|
|
||||||
),
|
|
||||||
http = Hysteria2Bean.Socks5Bean(
|
|
||||||
listen = "$LOOPBACK:${socksPort}",
|
|
||||||
),
|
|
||||||
tls = Hysteria2Bean.TlsBean(
|
|
||||||
sni = config.sni ?: config.server,
|
|
||||||
insecure = config.insecure,
|
|
||||||
pinSHA256 = if (config.pinSHA256.isNullOrEmpty()) null else config.pinSHA256
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return bean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a ProfileItem object to an OutboundBean object.
|
* Converts a ProfileItem object to an OutboundBean object.
|
||||||
*
|
*
|
||||||
@@ -145,7 +89,33 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
* @return the converted OutboundBean object, or null if conversion fails
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
*/
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HYSTERIA2)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HYSTERIA2) ?: return null
|
||||||
|
profileItem.network = NetworkType.HYSTERIA.type
|
||||||
|
profileItem.alpn = "h3"
|
||||||
|
|
||||||
|
outboundBean.settings?.let { server ->
|
||||||
|
server.address = getServerAddress(profileItem)
|
||||||
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
val sni = outboundBean.streamSettings?.let {
|
||||||
|
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundBean.streamSettings?.let {
|
||||||
|
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileItem.obfsPassword.isNotNullEmpty()) {
|
||||||
|
outboundBean.streamSettings?.udpmasks = mutableListOf(
|
||||||
|
StreamSettingsBean.UdpMasksBean(
|
||||||
|
type = "salamander",
|
||||||
|
settings = UdpMasksSettingsBean(
|
||||||
|
password = profileItem.obfsPassword
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,13 +110,6 @@ object AngConfigManager {
|
|||||||
if (guid == null) return -1
|
if (guid == null) return -1
|
||||||
val result = V2rayConfigManager.getV2rayConfig(context, guid)
|
val result = V2rayConfigManager.getV2rayConfig(context, guid)
|
||||||
if (result.status) {
|
if (result.status) {
|
||||||
val config = MmkvManager.decodeServerConfig(guid)
|
|
||||||
if (config?.configType == EConfigType.HYSTERIA2) {
|
|
||||||
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
|
|
||||||
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort)
|
|
||||||
Utils.setClipboard(context, JsonUtil.toJsonPretty(hy2Config) + "\n" + result.content)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
Utils.setClipboard(context, result.content)
|
Utils.setClipboard(context, result.content)
|
||||||
} else {
|
} else {
|
||||||
return -1
|
return -1
|
||||||
@@ -149,6 +142,7 @@ object AngConfigManager {
|
|||||||
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
|
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
|
||||||
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
|
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
|
||||||
EConfigType.POLICYGROUP -> ""
|
EConfigType.POLICYGROUP -> ""
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e)
|
Log.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e)
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
package com.v2ray.ang.handler
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.SystemClock
|
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.fmt.Hysteria2Fmt
|
|
||||||
import com.v2ray.ang.service.ProcessService
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
object PluginServiceManager {
|
|
||||||
private const val HYSTERIA2 = "libhysteria2.so"
|
|
||||||
|
|
||||||
private val procService: ProcessService by lazy {
|
|
||||||
ProcessService()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run the plugin based on the provided configuration.
|
|
||||||
*
|
|
||||||
* @param context The context to use.
|
|
||||||
* @param config The profile configuration.
|
|
||||||
* @param socksPort The port information.
|
|
||||||
*/
|
|
||||||
fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) {
|
|
||||||
Log.i(AppConfig.TAG, "Starting plugin execution")
|
|
||||||
|
|
||||||
if (config == null) {
|
|
||||||
Log.w(AppConfig.TAG, "Cannot run plugin: config is null")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (config.configType == EConfigType.HYSTERIA2) {
|
|
||||||
if (socksPort == null) {
|
|
||||||
Log.w(AppConfig.TAG, "Cannot run plugin: socksPort is null")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Log.i(AppConfig.TAG, "Running Hysteria2 plugin")
|
|
||||||
val configFile = genConfigHy2(context, config, socksPort) ?: return
|
|
||||||
val cmd = genCmdHy2(context, configFile)
|
|
||||||
|
|
||||||
procService.runProcess(context, cmd)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Error running plugin", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the running plugin.
|
|
||||||
*/
|
|
||||||
fun stopPlugin() {
|
|
||||||
stopHy2()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform a real ping using Hysteria2.
|
|
||||||
*
|
|
||||||
* @param context The context to use.
|
|
||||||
* @param config The profile configuration.
|
|
||||||
* @return The ping delay in milliseconds, or -1 if it fails.
|
|
||||||
*/
|
|
||||||
fun realPingHy2(context: Context, config: ProfileItem?): Long {
|
|
||||||
Log.i(AppConfig.TAG, "realPingHy2")
|
|
||||||
val retFailure = -1L
|
|
||||||
|
|
||||||
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
|
|
||||||
val socksPort = Utils.findFreePort(listOf(0))
|
|
||||||
val configFile = genConfigHy2(context, config, socksPort) ?: return retFailure
|
|
||||||
val cmd = genCmdHy2(context, configFile)
|
|
||||||
|
|
||||||
val proc = ProcessService()
|
|
||||||
proc.runProcess(context, cmd)
|
|
||||||
Thread.sleep(1000L)
|
|
||||||
val delay = SpeedtestManager.testConnection(context, socksPort)
|
|
||||||
proc.stopProcess()
|
|
||||||
|
|
||||||
return delay.first
|
|
||||||
}
|
|
||||||
return retFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate the configuration file for Hysteria2.
|
|
||||||
*
|
|
||||||
* @param context The context to use.
|
|
||||||
* @param config The profile configuration.
|
|
||||||
* @param socksPort The port information.
|
|
||||||
* @return The generated configuration file.
|
|
||||||
*/
|
|
||||||
private fun genConfigHy2(context: Context, config: ProfileItem, socksPort: Int): File? {
|
|
||||||
Log.i(AppConfig.TAG, "runPlugin $HYSTERIA2")
|
|
||||||
|
|
||||||
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null
|
|
||||||
|
|
||||||
val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
|
|
||||||
Log.i(AppConfig.TAG, "runPlugin ${configFile.absolutePath}")
|
|
||||||
|
|
||||||
configFile.parentFile?.mkdirs()
|
|
||||||
configFile.writeText(JsonUtil.toJson(hy2Config))
|
|
||||||
Log.i(AppConfig.TAG, JsonUtil.toJson(hy2Config))
|
|
||||||
|
|
||||||
return configFile
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate the command to run Hysteria2.
|
|
||||||
*
|
|
||||||
* @param context The context to use.
|
|
||||||
* @param configFile The configuration file.
|
|
||||||
* @return The command to run Hysteria2.
|
|
||||||
*/
|
|
||||||
private fun genCmdHy2(context: Context, configFile: File): MutableList<String> {
|
|
||||||
return mutableListOf(
|
|
||||||
File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath,
|
|
||||||
"--disable-update-check",
|
|
||||||
"--config",
|
|
||||||
configFile.absolutePath,
|
|
||||||
"--log-level",
|
|
||||||
"warn",
|
|
||||||
"client"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the Hysteria2 process.
|
|
||||||
*/
|
|
||||||
private fun stopHy2() {
|
|
||||||
try {
|
|
||||||
Log.i(AppConfig.TAG, "$HYSTERIA2 destroy")
|
|
||||||
procService.stopProcess()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to stop Hysteria2 process", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -168,7 +168,6 @@ object V2RayServiceManager {
|
|||||||
NotificationManager.showNotification(currentConfig)
|
NotificationManager.showNotification(currentConfig)
|
||||||
NotificationManager.startSpeedNotification(currentConfig)
|
NotificationManager.startSpeedNotification(currentConfig)
|
||||||
|
|
||||||
PluginServiceManager.runPlugin(service, config, result.socksPort)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to startup service", e)
|
Log.e(AppConfig.TAG, "Failed to startup service", e)
|
||||||
return false
|
return false
|
||||||
@@ -202,7 +201,6 @@ object V2RayServiceManager {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e)
|
Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e)
|
||||||
}
|
}
|
||||||
PluginServiceManager.stopPlugin()
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -16,6 +17,7 @@ import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean
|
|||||||
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
|
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.fmt.HttpFmt
|
import com.v2ray.ang.fmt.HttpFmt
|
||||||
|
import com.v2ray.ang.fmt.Hysteria2Fmt
|
||||||
import com.v2ray.ang.fmt.ShadowsocksFmt
|
import com.v2ray.ang.fmt.ShadowsocksFmt
|
||||||
import com.v2ray.ang.fmt.SocksFmt
|
import com.v2ray.ang.fmt.SocksFmt
|
||||||
import com.v2ray.ang.fmt.TrojanFmt
|
import com.v2ray.ang.fmt.TrojanFmt
|
||||||
@@ -43,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 {
|
||||||
@@ -66,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)
|
||||||
@@ -86,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 (SettingsManager.isUsingHevTun()) {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,12 +196,8 @@ object V2rayConfigManager {
|
|||||||
|
|
||||||
getInbounds(v2rayConfig)
|
getInbounds(v2rayConfig)
|
||||||
|
|
||||||
if (config.configType == EConfigType.HYSTERIA2) {
|
getOutbounds(v2rayConfig, config) ?: return result
|
||||||
result.socksPort = getPlusOutbounds(v2rayConfig, config) ?: return result
|
getMoreOutbounds(v2rayConfig, config.subscriptionId)
|
||||||
} else {
|
|
||||||
getOutbounds(v2rayConfig, config) ?: return result
|
|
||||||
getMoreOutbounds(v2rayConfig, config.subscriptionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
getRouting(v2rayConfig)
|
getRouting(v2rayConfig)
|
||||||
|
|
||||||
@@ -196,7 +228,6 @@ object V2rayConfigManager {
|
|||||||
val validConfigs = configList.asSequence().filter { it.server.isNotNullEmpty() }
|
val validConfigs = configList.asSequence().filter { it.server.isNotNullEmpty() }
|
||||||
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
||||||
.filter { it.configType != EConfigType.CUSTOM }
|
.filter { it.configType != EConfigType.CUSTOM }
|
||||||
.filter { it.configType != EConfigType.HYSTERIA2 }
|
|
||||||
.filter { it.configType != EConfigType.POLICYGROUP }
|
.filter { it.configType != EConfigType.POLICYGROUP }
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
@@ -270,12 +301,8 @@ object V2rayConfigManager {
|
|||||||
|
|
||||||
val v2rayConfig = initV2rayConfig(context) ?: return result
|
val v2rayConfig = initV2rayConfig(context) ?: return result
|
||||||
|
|
||||||
if (config.configType == EConfigType.HYSTERIA2) {
|
getOutbounds(v2rayConfig, config) ?: return result
|
||||||
result.socksPort = getPlusOutbounds(v2rayConfig, config) ?: return result
|
getMoreOutbounds(v2rayConfig, config.subscriptionId)
|
||||||
} else {
|
|
||||||
getOutbounds(v2rayConfig, config) ?: return result
|
|
||||||
getMoreOutbounds(v2rayConfig, config.subscriptionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
||||||
v2rayConfig.inbounds.clear()
|
v2rayConfig.inbounds.clear()
|
||||||
@@ -667,44 +694,6 @@ object V2rayConfigManager {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures special outbound settings for Hysteria2 protocol.
|
|
||||||
*
|
|
||||||
* Creates a SOCKS outbound connection on a free port for protocols requiring special handling.
|
|
||||||
*
|
|
||||||
* @param v2rayConfig The V2ray configuration object to be modified
|
|
||||||
* @param config The profile item containing connection details
|
|
||||||
* @return The port number for the SOCKS connection, or null if there was an error
|
|
||||||
*/
|
|
||||||
private fun getPlusOutbounds(v2rayConfig: V2rayConfig, config: ProfileItem): Int? {
|
|
||||||
try {
|
|
||||||
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
|
|
||||||
|
|
||||||
val outboundNew = OutboundBean(
|
|
||||||
mux = null,
|
|
||||||
protocol = EConfigType.SOCKS.name.lowercase(),
|
|
||||||
settings = OutSettingsBean(
|
|
||||||
servers = listOf(
|
|
||||||
OutSettingsBean.ServersBean(
|
|
||||||
address = AppConfig.LOOPBACK,
|
|
||||||
port = socksPort
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (v2rayConfig.outbounds.isNotEmpty()) {
|
|
||||||
v2rayConfig.outbounds[0] = outboundNew
|
|
||||||
} else {
|
|
||||||
v2rayConfig.outbounds.add(outboundNew)
|
|
||||||
}
|
|
||||||
|
|
||||||
return socksPort
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to configure plusOutbound", e)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures additional outbound connections for proxy chaining.
|
* Configures additional outbound connections for proxy chaining.
|
||||||
*
|
*
|
||||||
@@ -1046,9 +1035,10 @@ object V2rayConfigManager {
|
|||||||
EConfigType.VLESS -> VlessFmt.toOutbound(profileItem)
|
EConfigType.VLESS -> VlessFmt.toOutbound(profileItem)
|
||||||
EConfigType.TROJAN -> TrojanFmt.toOutbound(profileItem)
|
EConfigType.TROJAN -> TrojanFmt.toOutbound(profileItem)
|
||||||
EConfigType.WIREGUARD -> WireguardFmt.toOutbound(profileItem)
|
EConfigType.WIREGUARD -> WireguardFmt.toOutbound(profileItem)
|
||||||
EConfigType.HYSTERIA2 -> null
|
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toOutbound(profileItem)
|
||||||
EConfigType.HTTP -> HttpFmt.toOutbound(profileItem)
|
EConfigType.HTTP -> HttpFmt.toOutbound(profileItem)
|
||||||
EConfigType.POLICYGROUP -> null
|
EConfigType.POLICYGROUP -> null
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1079,8 +1069,7 @@ object V2rayConfigManager {
|
|||||||
EConfigType.SHADOWSOCKS,
|
EConfigType.SHADOWSOCKS,
|
||||||
EConfigType.SOCKS,
|
EConfigType.SOCKS,
|
||||||
EConfigType.HTTP,
|
EConfigType.HTTP,
|
||||||
EConfigType.TROJAN,
|
EConfigType.TROJAN ->
|
||||||
EConfigType.HYSTERIA2 ->
|
|
||||||
return OutboundBean(
|
return OutboundBean(
|
||||||
protocol = configType.name.lowercase(),
|
protocol = configType.name.lowercase(),
|
||||||
settings = OutSettingsBean(
|
settings = OutSettingsBean(
|
||||||
@@ -1098,6 +1087,16 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
EConfigType.HYSTERIA,
|
||||||
|
EConfigType.HYSTERIA2 ->
|
||||||
|
return OutboundBean(
|
||||||
|
protocol = EConfigType.HYSTERIA.name.lowercase(),
|
||||||
|
settings = OutSettingsBean(
|
||||||
|
servers = null
|
||||||
|
),
|
||||||
|
streamSettings = StreamSettingsBean()
|
||||||
|
)
|
||||||
|
|
||||||
EConfigType.CUSTOM -> null
|
EConfigType.CUSTOM -> null
|
||||||
EConfigType.POLICYGROUP -> null
|
EConfigType.POLICYGROUP -> null
|
||||||
}
|
}
|
||||||
@@ -1127,7 +1126,7 @@ object V2rayConfigManager {
|
|||||||
val xhttpExtra = profileItem.xhttpExtra
|
val xhttpExtra = profileItem.xhttpExtra
|
||||||
|
|
||||||
var sni: String? = null
|
var sni: String? = null
|
||||||
streamSettings.network = if (transport.isEmpty()) NetworkType.TCP.type else transport
|
streamSettings.network = transport.ifEmpty { NetworkType.TCP.type }
|
||||||
when (streamSettings.network) {
|
when (streamSettings.network) {
|
||||||
NetworkType.TCP.type -> {
|
NetworkType.TCP.type -> {
|
||||||
val tcpSetting = StreamSettingsBean.TcpSettingsBean()
|
val tcpSetting = StreamSettingsBean.TcpSettingsBean()
|
||||||
@@ -1216,6 +1215,27 @@ object V2rayConfigManager {
|
|||||||
sni = authority
|
sni = authority
|
||||||
streamSettings.grpcSettings = grpcSetting
|
streamSettings.grpcSettings = grpcSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NetworkType.HYSTERIA.type -> {
|
||||||
|
val hysteriaSetting = StreamSettingsBean.HysteriaSettingsBean(
|
||||||
|
version = 2,
|
||||||
|
auth = profileItem.password.orEmpty(),
|
||||||
|
up = profileItem.bandwidthUp?.ifEmpty { "0" }.orEmpty(),
|
||||||
|
down = profileItem.bandwidthDown?.ifEmpty { "0" }.orEmpty(),
|
||||||
|
udphop = null
|
||||||
|
)
|
||||||
|
if (profileItem.portHopping.isNotNullEmpty()) {
|
||||||
|
hysteriaSetting.udphop = StreamSettingsBean.HysteriaSettingsBean.HysteriaUdpHopBean(
|
||||||
|
port = profileItem.portHopping,
|
||||||
|
interval = profileItem.portHoppingInterval
|
||||||
|
?.trim()
|
||||||
|
?.toIntOrNull()
|
||||||
|
?.takeIf { it >= 5 }
|
||||||
|
?: 30
|
||||||
|
)
|
||||||
|
}
|
||||||
|
streamSettings.hysteriaSettings = hysteriaSetting
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return sni
|
return sni
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,18 +35,18 @@ object WebDavManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload a local file to a remote relative path under the configured remoteBasePath.
|
* Upload a local file to a remote file name under the configured remoteBasePath.
|
||||||
* The provided `remoteRelativePath` should be relative (e.g. "backup_ng.zip").
|
* The provided `remoteFileName` should be a file name (e.g. "backup_ng.zip").
|
||||||
* The method will attempt to create parent directories via MKCOL before PUT.
|
* The method will attempt to create parent directories via MKCOL before PUT.
|
||||||
*
|
*
|
||||||
* @param localFile File to upload.
|
* @param localFile File to upload.
|
||||||
* @param remoteRelativePath Remote path relative to configured remoteBasePath.
|
* @param remoteFileName Remote file name relative to configured remoteBasePath.
|
||||||
* @return true if upload succeeded (HTTP 2xx), false otherwise.
|
* @return true if upload succeeded (HTTP 2xx), false otherwise.
|
||||||
*/
|
*/
|
||||||
suspend fun uploadFile(localFile: File, remoteRelativePath: String): Boolean = withContext(Dispatchers.IO) {
|
suspend fun uploadFile(localFile: File, remoteFileName: String): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
val remote = buildRemoteUrl(remoteFileName)
|
||||||
try {
|
try {
|
||||||
val cl = client ?: return@withContext false
|
val cl = client ?: return@withContext false
|
||||||
val remote = buildRemoteUrl(remoteRelativePath)
|
|
||||||
|
|
||||||
// Ensure parent directories exist
|
// Ensure parent directories exist
|
||||||
val dirPath = remote.substringBeforeLast('/')
|
val dirPath = remote.substringBeforeLast('/')
|
||||||
@@ -67,14 +67,14 @@ object WebDavManager {
|
|||||||
cl.newCall(req).execute().use { resp ->
|
cl.newCall(req).execute().use { resp ->
|
||||||
val success = resp.isSuccessful
|
val success = resp.isSuccessful
|
||||||
if (success) {
|
if (success) {
|
||||||
Log.i(AppConfig.TAG, "WebDAV upload success: $remoteRelativePath")
|
Log.i(AppConfig.TAG, "WebDAV upload success: $remote")
|
||||||
} else {
|
} else {
|
||||||
Log.e(AppConfig.TAG, "WebDAV upload failed: $remoteRelativePath (HTTP ${resp.code})")
|
Log.e(AppConfig.TAG, "WebDAV upload failed: $remote (HTTP ${resp.code})")
|
||||||
}
|
}
|
||||||
return@withContext success
|
return@withContext success
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "WebDAV upload exception: $remoteRelativePath", e)
|
Log.e(AppConfig.TAG, "WebDAV upload exception: $remote", e)
|
||||||
return@withContext false
|
return@withContext false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,18 +82,18 @@ object WebDavManager {
|
|||||||
/**
|
/**
|
||||||
* Download a remote file (relative to configured remoteBasePath) into a local file.
|
* Download a remote file (relative to configured remoteBasePath) into a local file.
|
||||||
*
|
*
|
||||||
* @param remoteRelativePath Remote path relative to configured remoteBasePath.
|
* @param remoteFileName Remote file name relative to configured remoteBasePath.
|
||||||
* @param destFile Local destination file to write to.
|
* @param destFile Local destination file to write to.
|
||||||
* @return true if download and write succeeded, false otherwise.
|
* @return true if download and write succeeded, false otherwise.
|
||||||
*/
|
*/
|
||||||
suspend fun downloadFile(remoteRelativePath: String, destFile: File): Boolean = withContext(Dispatchers.IO) {
|
suspend fun downloadFile(remoteFileName: String, destFile: File): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
val remote = buildRemoteUrl(remoteFileName)
|
||||||
try {
|
try {
|
||||||
val cl = client ?: return@withContext false
|
val cl = client ?: return@withContext false
|
||||||
val remote = buildRemoteUrl(remoteRelativePath)
|
|
||||||
val req = applyAuth(Request.Builder().url(remote).get()).build()
|
val req = applyAuth(Request.Builder().url(remote).get()).build()
|
||||||
cl.newCall(req).execute().use { resp ->
|
cl.newCall(req).execute().use { resp ->
|
||||||
if (!resp.isSuccessful) {
|
if (!resp.isSuccessful) {
|
||||||
Log.e(AppConfig.TAG, "WebDAV download failed: $remoteRelativePath (HTTP ${resp.code})")
|
Log.e(AppConfig.TAG, "WebDAV download failed: $remote (HTTP ${resp.code})")
|
||||||
return@withContext false
|
return@withContext false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,29 +104,31 @@ object WebDavManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(AppConfig.TAG, "WebDAV download success: $remoteRelativePath")
|
Log.i(AppConfig.TAG, "WebDAV download success: $remote")
|
||||||
return@withContext true
|
return@withContext true
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "WebDAV download exception: $remoteRelativePath", e)
|
Log.e(AppConfig.TAG, "WebDAV download exception: $remote", e)
|
||||||
return@withContext false
|
return@withContext false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a full remote URL by combining the configured base URL, the configured
|
* Build a full remote URL by combining the configured base URL, the configured
|
||||||
* remote base path and a relative path provided by the caller.
|
* remote base path and a file name provided by the caller.
|
||||||
*
|
*
|
||||||
* Example: baseUrl="https://example.com/remote.php/dav", remoteBasePath="backups",
|
* Example: baseUrl="https://example.com/remote.php/dav", remoteBasePath="backups",
|
||||||
* remoteRelativePath="backup_ng.zip" => "https://example.com/remote.php/dav/backups/backup_ng.zip"
|
* remoteFileName="backup_ng.zip" => "https://example.com/remote.php/dav/backups/backup_ng.zip"
|
||||||
*
|
*
|
||||||
* @param remoteRelativePath A path relative to the configured remoteBasePath (no leading slash required).
|
* @param remoteFileName A file name relative to the configured remoteBasePath (no leading slash required).
|
||||||
* @return Full URL string used for HTTP operations.
|
* @return Full URL string used for HTTP operations.
|
||||||
*/
|
*/
|
||||||
private fun buildRemoteUrl(remoteRelativePath: String): String {
|
private fun buildRemoteUrl(remoteFileName: String): String {
|
||||||
val base = cfg?.baseUrl?.trimEnd('/') ?: ""
|
val base = cfg?.baseUrl?.trimEnd('/') ?: ""
|
||||||
val basePath = cfg?.remoteBasePath?.trim('/') ?: ""
|
// Use configured remoteBasePath when not empty; otherwise fallback to AppConfig.WEBDAV_BACKUP_DIR
|
||||||
val rel = remoteRelativePath.trimStart('/')
|
val basePathConfigured = cfg?.remoteBasePath?.trim('/')?.takeIf { it.isNotEmpty() }
|
||||||
|
val basePath = basePathConfigured ?: AppConfig.WEBDAV_BACKUP_DIR
|
||||||
|
val rel = remoteFileName.trimStart('/')
|
||||||
return if (basePath.isEmpty()) "$base/$rel" else "$base/$basePath/$rel"
|
return if (basePath.isEmpty()) "$base/$rel" else "$base/$basePath/$rel"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +172,7 @@ object WebDavManager {
|
|||||||
Log.w(AppConfig.TAG, "WebDAV MKCOL $mkUrl returned ${resp.code}")
|
Log.w(AppConfig.TAG, "WebDAV MKCOL $mkUrl returned ${resp.code}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ignored: Exception) {
|
} catch (_: Exception) {
|
||||||
// best-effort, continue
|
// best-effort, continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
/******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
|
||||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
|
||||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
******************************************************************************/
|
|
||||||
|
|
||||||
package com.v2ray.ang.plugin
|
|
||||||
|
|
||||||
import android.content.pm.ResolveInfo
|
|
||||||
|
|
||||||
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
|
|
||||||
init {
|
|
||||||
check(resolveInfo.providerInfo != null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val componentInfo get() = resolveInfo.providerInfo!!
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
|
||||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
|
||||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
******************************************************************************/
|
|
||||||
|
|
||||||
package com.v2ray.ang.plugin
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
|
|
||||||
abstract class Plugin {
|
|
||||||
abstract val id: String
|
|
||||||
abstract val label: CharSequence
|
|
||||||
abstract val version: Int
|
|
||||||
abstract val versionName: String
|
|
||||||
open val icon: Drawable? get() = null
|
|
||||||
open val defaultConfig: String? get() = null
|
|
||||||
open val packageName: String get() = ""
|
|
||||||
open val directBootAware: Boolean get() = true
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
return id == (other as Plugin).id
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode() = id.hashCode()
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
******************************************************************************/
|
|
||||||
|
|
||||||
package com.v2ray.ang.plugin
|
|
||||||
|
|
||||||
object PluginContract {
|
|
||||||
|
|
||||||
const val ACTION_NATIVE_PLUGIN = "io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN"
|
|
||||||
const val EXTRA_ENTRY = "io.nekohasekai.sagernet.plugin.EXTRA_ENTRY"
|
|
||||||
const val METADATA_KEY_ID = "io.nekohasekai.sagernet.plugin.id"
|
|
||||||
const val METADATA_KEY_EXECUTABLE_PATH = "io.nekohasekai.sagernet.plugin.executable_path"
|
|
||||||
const val METHOD_GET_EXECUTABLE = "sagernet:getExecutable"
|
|
||||||
|
|
||||||
const val COLUMN_PATH = "path"
|
|
||||||
const val COLUMN_MODE = "mode"
|
|
||||||
const val SCHEME = "plugin"
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
/******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
|
||||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
|
||||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
******************************************************************************/
|
|
||||||
|
|
||||||
package com.v2ray.ang.plugin
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import com.v2ray.ang.AngApplication
|
|
||||||
|
|
||||||
class PluginList : ArrayList<Plugin>() {
|
|
||||||
init {
|
|
||||||
addAll(
|
|
||||||
AngApplication.application.packageManager.queryIntentContentProviders(
|
|
||||||
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA
|
|
||||||
)
|
|
||||||
.filter { it.providerInfo.exported }.map { NativePlugin(it) })
|
|
||||||
}
|
|
||||||
|
|
||||||
val lookup = mutableMapOf<String, Plugin>().apply {
|
|
||||||
for (plugin in this@PluginList.toList()) {
|
|
||||||
fun check(old: Plugin?) {
|
|
||||||
if (old != null && old != plugin) {
|
|
||||||
this@PluginList.remove(old)
|
|
||||||
}
|
|
||||||
/* if (old != null && old !== plugin) {
|
|
||||||
val packages = this@PluginList.filter { it.id == plugin.id }
|
|
||||||
.joinToString { it.packageName }
|
|
||||||
val message = "Conflicting plugins found from: $packages"
|
|
||||||
Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()
|
|
||||||
throw IllegalStateException(message)
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
check(put(plugin.id, plugin))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
/******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2021 by nekohasekai <contact-AngApplication@sekai.icu> *
|
|
||||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
|
||||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
******************************************************************************/
|
|
||||||
|
|
||||||
package com.v2ray.ang.plugin
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.ComponentInfo
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.content.pm.ProviderInfo
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.system.Os
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import com.v2ray.ang.AngApplication
|
|
||||||
import com.v2ray.ang.extension.listenForPackageChanges
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
|
|
||||||
object PluginManager {
|
|
||||||
|
|
||||||
class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin)
|
|
||||||
|
|
||||||
private var receiver: BroadcastReceiver? = null
|
|
||||||
private var cachedPlugins: PluginList? = null
|
|
||||||
fun fetchPlugins() = synchronized(this) {
|
|
||||||
if (receiver == null) receiver = AngApplication.application.listenForPackageChanges {
|
|
||||||
synchronized(this) {
|
|
||||||
receiver = null
|
|
||||||
cachedPlugins = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (cachedPlugins == null) cachedPlugins = PluginList()
|
|
||||||
cachedPlugins!!
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildUri(id: String, authority: String) = Uri.Builder()
|
|
||||||
.scheme(PluginContract.SCHEME)
|
|
||||||
.authority(authority)
|
|
||||||
.path("/$id")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
data class InitResult(
|
|
||||||
val path: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Throws(Throwable::class)
|
|
||||||
fun init(pluginId: String): InitResult? {
|
|
||||||
if (pluginId.isEmpty()) return null
|
|
||||||
var throwable: Throwable? = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
val result = initNative(pluginId)
|
|
||||||
if (result != null) return result
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
if (throwable == null) throwable = t //Logs.w(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw throwable ?: PluginNotFoundException(pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initNative(pluginId: String): InitResult? {
|
|
||||||
var flags = PackageManager.GET_META_DATA
|
|
||||||
if (Build.VERSION.SDK_INT >= 24) {
|
|
||||||
flags =
|
|
||||||
flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE
|
|
||||||
}
|
|
||||||
var providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
|
||||||
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "com.github.dyhkwong.AngApplication")), flags
|
|
||||||
)
|
|
||||||
.filter { it.providerInfo.exported }
|
|
||||||
if (providers.isEmpty()) {
|
|
||||||
providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
|
||||||
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "io.nekohasekai.AngApplication")), flags
|
|
||||||
)
|
|
||||||
.filter { it.providerInfo.exported }
|
|
||||||
}
|
|
||||||
if (providers.isEmpty()) {
|
|
||||||
providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
|
||||||
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "moe.matsuri.lite")), flags
|
|
||||||
)
|
|
||||||
.filter { it.providerInfo.exported }
|
|
||||||
}
|
|
||||||
if (providers.isEmpty()) {
|
|
||||||
providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
|
||||||
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "fr.husi")), flags
|
|
||||||
)
|
|
||||||
.filter { it.providerInfo.exported }
|
|
||||||
}
|
|
||||||
if (providers.isEmpty()) {
|
|
||||||
providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
|
||||||
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA
|
|
||||||
).filter {
|
|
||||||
it.providerInfo.exported &&
|
|
||||||
it.providerInfo.metaData.containsKey(METADATA_KEY_ID) &&
|
|
||||||
it.providerInfo.metaData.getString(METADATA_KEY_ID) == pluginId
|
|
||||||
}
|
|
||||||
if (providers.size > 1) {
|
|
||||||
providers = listOf(providers[0]) // What if there is more than one?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (providers.isEmpty()) return null
|
|
||||||
if (providers.size > 1) {
|
|
||||||
val message =
|
|
||||||
"Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
|
|
||||||
AngApplication.application.toast(message)
|
|
||||||
throw IllegalStateException(message)
|
|
||||||
}
|
|
||||||
val provider = providers.single().providerInfo
|
|
||||||
var failure: Throwable? = null
|
|
||||||
try {
|
|
||||||
initNativeFaster(provider)?.also { return InitResult(it) }
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
// Logs.w("Initializing native plugin faster mode failed")
|
|
||||||
failure = t
|
|
||||||
}
|
|
||||||
|
|
||||||
val uri = Uri.Builder().apply {
|
|
||||||
scheme(ContentResolver.SCHEME_CONTENT)
|
|
||||||
authority(provider.authority)
|
|
||||||
}.build()
|
|
||||||
try {
|
|
||||||
return initNativeFast(
|
|
||||||
AngApplication.application.contentResolver,
|
|
||||||
pluginId,
|
|
||||||
uri
|
|
||||||
)?.let { InitResult(it) }
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
// Logs.w("Initializing native plugin fast mode failed")
|
|
||||||
failure?.also { t.addSuppressed(it) }
|
|
||||||
failure = t
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return initNativeSlow(
|
|
||||||
AngApplication.application.contentResolver,
|
|
||||||
pluginId,
|
|
||||||
uri
|
|
||||||
)?.let { InitResult(it) }
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
failure.also { t.addSuppressed(it) }
|
|
||||||
throw t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initNativeFaster(provider: ProviderInfo): String? {
|
|
||||||
return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH)
|
|
||||||
?.let { relativePath ->
|
|
||||||
File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply {
|
|
||||||
check(canExecute())
|
|
||||||
}.absolutePath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initNativeFast(cr: ContentResolver, pluginId: String, uri: Uri): String? {
|
|
||||||
return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, bundleOf())
|
|
||||||
?.getString(PluginContract.EXTRA_ENTRY)?.also {
|
|
||||||
check(File(it).canExecute())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("Recycle")
|
|
||||||
private fun initNativeSlow(cr: ContentResolver, pluginId: String, uri: Uri): String? {
|
|
||||||
var initialized = false
|
|
||||||
fun entryNotFound(): Nothing =
|
|
||||||
throw IndexOutOfBoundsException("Plugin entry binary not found")
|
|
||||||
|
|
||||||
val pluginDir = File(AngApplication.application.noBackupFilesDir, "plugin")
|
|
||||||
(cr.query(
|
|
||||||
uri,
|
|
||||||
arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
?: return null).use { cursor ->
|
|
||||||
if (!cursor.moveToFirst()) entryNotFound()
|
|
||||||
pluginDir.deleteRecursively()
|
|
||||||
if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory")
|
|
||||||
val pluginDirPath = pluginDir.absolutePath + '/'
|
|
||||||
do {
|
|
||||||
val path = cursor.getString(0)
|
|
||||||
val file = File(pluginDir, path)
|
|
||||||
check(file.absolutePath.startsWith(pluginDirPath))
|
|
||||||
cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->
|
|
||||||
file.outputStream().use { outStream -> inStream.copyTo(outStream) }
|
|
||||||
}
|
|
||||||
Os.chmod(
|
|
||||||
file.absolutePath, when (cursor.getType(1)) {
|
|
||||||
Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1)
|
|
||||||
Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8)
|
|
||||||
else -> throw IllegalArgumentException("File mode should be of type int")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (path == pluginId) initialized = true
|
|
||||||
} while (cursor.moveToNext())
|
|
||||||
}
|
|
||||||
if (!initialized) entryNotFound()
|
|
||||||
return File(pluginDir, pluginId).absolutePath
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ComponentInfo.loadString(key: String) = when (val value = metaData.getString(key)) {
|
|
||||||
is String -> value
|
|
||||||
// is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
|
|
||||||
// .getString(value)
|
|
||||||
|
|
||||||
null -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
|
||||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
|
||||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
******************************************************************************/
|
|
||||||
|
|
||||||
package com.v2ray.ang.plugin
|
|
||||||
|
|
||||||
import android.content.pm.ComponentInfo
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.content.pm.ResolveInfo
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.os.Build
|
|
||||||
import com.v2ray.ang.AngApplication
|
|
||||||
import com.v2ray.ang.plugin.PluginManager.loadString
|
|
||||||
|
|
||||||
abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
|
|
||||||
protected abstract val componentInfo: ComponentInfo
|
|
||||||
|
|
||||||
override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! }
|
|
||||||
override val version by lazy {
|
|
||||||
getPackageInfo(componentInfo.packageName).versionCode
|
|
||||||
}
|
|
||||||
override val versionName: String by lazy {
|
|
||||||
getPackageInfo(componentInfo.packageName).versionName!!
|
|
||||||
}
|
|
||||||
override val label: CharSequence get() = resolveInfo.loadLabel(AngApplication.application.packageManager)
|
|
||||||
override val icon: Drawable get() = resolveInfo.loadIcon(AngApplication.application.packageManager)
|
|
||||||
override val packageName: String get() = componentInfo.packageName
|
|
||||||
override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware
|
|
||||||
|
|
||||||
fun getPackageInfo(packageName: String) = AngApplication.application.packageManager.getPackageInfo(
|
|
||||||
packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
|
||||||
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES
|
|
||||||
)!!
|
|
||||||
}
|
|
||||||
@@ -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,37 +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.dto.EConfigType
|
|
||||||
import com.v2ray.ang.extension.serializable
|
import com.v2ray.ang.extension.serializable
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.PluginServiceManager
|
|
||||||
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.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancelChildren
|
|
||||||
import kotlinx.coroutines.joinAll
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
|
||||||
|
|
||||||
class V2RayTestService : Service() {
|
class V2RayTestService : Service() {
|
||||||
private val realTestJob = SupervisorJob()
|
|
||||||
private val realDispatcher = Dispatchers.IO.limitedParallelism(
|
|
||||||
Runtime.getRuntime().availableProcessors() * 3
|
|
||||||
)
|
|
||||||
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.
|
||||||
@@ -60,7 +38,10 @@ class V2RayTestService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
realTestJob.cancel()
|
// cancel any active workers
|
||||||
|
val snapshot = ArrayList(activeWorkers)
|
||||||
|
snapshot.forEach { it.cancel() }
|
||||||
|
activeWorkers.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,70 +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 config = MmkvManager.decodeServerConfig(guid) ?: return retFailure
|
|
||||||
if (config.configType == EConfigType.HYSTERIA2) {
|
|
||||||
val delay = PluginServiceManager.realPingHy2(this, config)
|
|
||||||
return delay
|
|
||||||
} else {
|
|
||||||
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(this, guid)
|
|
||||||
if (!configResult.status) {
|
|
||||||
return retFailure
|
|
||||||
}
|
|
||||||
return V2RayNativeManager.measureOutboundDelay(configResult.content, SettingsManager.getDelayTestUrl())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ import androidx.core.content.FileProvider
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.WEBDAV_BACKUP_FILE_NAME
|
||||||
import com.v2ray.ang.BuildConfig
|
import com.v2ray.ang.BuildConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityBackupBinding
|
import com.v2ray.ang.databinding.ActivityBackupBinding
|
||||||
@@ -40,10 +41,6 @@ class BackupActivity : BaseActivity() {
|
|||||||
resources.getStringArray(R.array.config_backup_options)
|
resources.getStringArray(R.array.config_backup_options)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val BACKUP_FILE_NAME = "backup_ng.zip"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val requestPermissionLauncher =
|
private val requestPermissionLauncher =
|
||||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||||
if (isGranted) {
|
if (isGranted) {
|
||||||
@@ -263,7 +260,7 @@ class BackupActivity : BaseActivity() {
|
|||||||
WebDavManager.init(saved)
|
WebDavManager.init(saved)
|
||||||
|
|
||||||
val ok = try {
|
val ok = try {
|
||||||
WebDavManager.uploadFile(tempFile, BACKUP_FILE_NAME)
|
WebDavManager.uploadFile(tempFile, WEBDAV_BACKUP_FILE_NAME)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "WebDAV upload error", e)
|
Log.e(AppConfig.TAG, "WebDAV upload error", e)
|
||||||
false
|
false
|
||||||
@@ -303,7 +300,7 @@ class BackupActivity : BaseActivity() {
|
|||||||
try {
|
try {
|
||||||
target = File(cacheDir, "download_${System.currentTimeMillis()}.zip")
|
target = File(cacheDir, "download_${System.currentTimeMillis()}.zip")
|
||||||
WebDavManager.init(saved)
|
WebDavManager.init(saved)
|
||||||
val ok = WebDavManager.downloadFile(BACKUP_FILE_NAME, target)
|
val ok = WebDavManager.downloadFile(WEBDAV_BACKUP_FILE_NAME, target)
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
toastError(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
@@ -351,7 +348,7 @@ class BackupActivity : BaseActivity() {
|
|||||||
val url = dialogBinding.etWebdavUrl.text.toString().trim()
|
val url = dialogBinding.etWebdavUrl.text.toString().trim()
|
||||||
val user = dialogBinding.etWebdavUser.text.toString().trim().ifEmpty { null }
|
val user = dialogBinding.etWebdavUser.text.toString().trim().ifEmpty { null }
|
||||||
val pass = dialogBinding.etWebdavPass.text.toString()
|
val pass = dialogBinding.etWebdavPass.text.toString()
|
||||||
val remotePath = dialogBinding.etWebdavRemotePath.text.toString().trim().ifEmpty { "/" }
|
val remotePath = dialogBinding.etWebdavRemotePath.text.toString().trim().ifEmpty { AppConfig.WEBDAV_BACKUP_DIR }
|
||||||
val cfg = WebDavConfig(baseUrl = url, username = user, password = pass, remoteBasePath = remotePath)
|
val cfg = WebDavConfig(baseUrl = url, username = user, password = pass, remoteBasePath = remotePath)
|
||||||
MmkvManager.encodeWebDavConfig(cfg)
|
MmkvManager.encodeWebDavConfig(cfg)
|
||||||
toastSuccess(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
|
|||||||
@@ -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 ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
|
||||||
|
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 {
|
||||||
@@ -280,21 +281,22 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
* @param position The position in the list
|
* @param position The position in the list
|
||||||
*/
|
*/
|
||||||
private fun removeServer(guid: String, position: Int) {
|
private fun removeServer(guid: String, position: Int) {
|
||||||
if (guid != MmkvManager.getSelectServer()) {
|
if (guid == MmkvManager.getSelectServer()) {
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
|
|
||||||
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
removeServerSub(guid, position)
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
} else {
|
|
||||||
removeServerSub(guid, position)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
application.toast(R.string.toast_action_not_allowed)
|
application.toast(R.string.toast_action_not_allowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
|
||||||
|
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
removeServerSub(guid, position)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
removeServerSub(guid, position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,8 +307,12 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
*/
|
*/
|
||||||
private fun removeServerSub(guid: String, position: Int) {
|
private fun removeServerSub(guid: String, position: Int) {
|
||||||
mActivity.mainViewModel.removeServer(guid)
|
mActivity.mainViewModel.removeServer(guid)
|
||||||
notifyItemRemoved(position)
|
val idx = data.indexOfFirst { it.guid == guid }
|
||||||
notifyItemRangeChanged(position, data.size)
|
if (idx >= 0) {
|
||||||
|
data.removeAt(idx)
|
||||||
|
notifyItemRemoved(idx)
|
||||||
|
notifyItemRangeChanged(idx, data.size - idx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -364,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
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.ActivityNoneBinding
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
@@ -21,7 +22,7 @@ import io.github.g00fy2.quickie.ScanCustomCode
|
|||||||
import io.github.g00fy2.quickie.config.ScannerConfig
|
import io.github.g00fy2.quickie.config.ScannerConfig
|
||||||
|
|
||||||
class ScannerActivity : BaseActivity() {
|
class ScannerActivity : BaseActivity() {
|
||||||
|
private val binding by lazy { ActivityNoneBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
|
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
|
||||||
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
@@ -59,6 +60,8 @@ class ScannerActivity : BaseActivity() {
|
|||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.menu_item_import_config_qrcode))
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_START_SCAN_IMMEDIATE)) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_START_SCAN_IMMEDIATE)) {
|
||||||
launchScan()
|
launchScan()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
EConfigType.WIREGUARD -> R.layout.activity_server_wireguard
|
EConfigType.WIREGUARD -> R.layout.activity_server_wireguard
|
||||||
EConfigType.HYSTERIA2 -> R.layout.activity_server_hysteria2
|
EConfigType.HYSTERIA2 -> R.layout.activity_server_hysteria2
|
||||||
EConfigType.POLICYGROUP -> null
|
EConfigType.POLICYGROUP -> null
|
||||||
|
else -> null
|
||||||
} ?: return
|
} ?: return
|
||||||
setContentViewWithToolbar(layoutId, showHomeAsUp = true, title = (config?.configType ?: createConfigType).toString())
|
setContentViewWithToolbar(layoutId, showHomeAsUp = true, title = (config?.configType ?: createConfigType).toString())
|
||||||
|
|
||||||
|
|||||||
@@ -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,19 +50,8 @@ 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 hevTunRwTimeout by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT) }
|
|
||||||
// private val useTun by lazy { findPreference<ListPreference>(AppConfig.PREF_TUN) }
|
|
||||||
|
|
||||||
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
|
||||||
// This prevents inconsistencies between SharedPreferences and MMKV
|
// This prevents inconsistencies between SharedPreferences and MMKV
|
||||||
@@ -77,30 +61,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 +83,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 +93,12 @@ class SettingsActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
true
|
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 ->
|
mode?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
updateMode(newValue.toString())
|
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 ->
|
|
||||||
// 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
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initPreferenceSummaries() {
|
private fun initPreferenceSummaries() {
|
||||||
@@ -212,6 +111,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 +121,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is CheckBoxPreference, is androidx.preference.SwitchPreferenceCompat -> {
|
is CheckBoxPreference, is androidx.preference.SwitchPreferenceCompat -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,100 +153,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() {
|
|
||||||
// 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?) {
|
private fun updateMode(mode: String?) {
|
||||||
val vpn = mode == VPN
|
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
|
||||||
@@ -423,29 +234,7 @@ 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?) {
|
|
||||||
// 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
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onModeHelpClicked(view: View) {
|
fun onModeHelpClicked(view: View) {
|
||||||
|
|||||||
@@ -375,24 +375,6 @@ object Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the path to the backup directory.
|
|
||||||
*
|
|
||||||
* @param context The context to use.
|
|
||||||
* @return The path to the backup directory.
|
|
||||||
*/
|
|
||||||
fun backupPath(context: Context?): String {
|
|
||||||
if (context == null) return ""
|
|
||||||
|
|
||||||
return try {
|
|
||||||
context.getExternalFilesDir(AppConfig.DIR_BACKUPS)?.absolutePath
|
|
||||||
?: context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to get backup path", e)
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the device ID for XUDP base key.
|
* Get the device ID for XUDP base key.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -388,6 +388,7 @@
|
|||||||
<item>চায়না ব্ল্যাকলিস্ট</item>
|
<item>চায়না ব্ল্যাকলিস্ট</item>
|
||||||
<item>গ্লোবাল</item>
|
<item>গ্লোবাল</item>
|
||||||
<item>ইরান হোয়াইটলিস্ট</item>
|
<item>ইরান হোয়াইটলিস্ট</item>
|
||||||
|
<item>রাশিয়া হোয়াইটলিস্ট</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="vpn_bypass_lan">
|
<string-array name="vpn_bypass_lan">
|
||||||
|
|||||||
@@ -398,6 +398,7 @@
|
|||||||
<item>نومگه شه چین</item>
|
<item>نومگه شه چین</item>
|
||||||
<item>جهۊوی (Global)</item>
|
<item>جهۊوی (Global)</item>
|
||||||
<item>نومگه اسبؽڌ ایران</item>
|
<item>نومگه اسبؽڌ ایران</item>
|
||||||
|
<item>Russia Whitelist</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="vpn_bypass_lan">
|
<string-array name="vpn_bypass_lan">
|
||||||
|
|||||||
@@ -397,6 +397,7 @@
|
|||||||
<item>لیست سیاه چین</item>
|
<item>لیست سیاه چین</item>
|
||||||
<item>جهانی(GLOBAL)</item>
|
<item>جهانی(GLOBAL)</item>
|
||||||
<item>ایران</item>
|
<item>ایران</item>
|
||||||
|
<item>لیست سفید روسیه</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="vpn_bypass_lan">
|
<string-array name="vpn_bypass_lan">
|
||||||
|
|||||||
@@ -397,6 +397,7 @@
|
|||||||
<item>Чёрный список Китая</item>
|
<item>Чёрный список Китая</item>
|
||||||
<item>Общие</item>
|
<item>Общие</item>
|
||||||
<item>Белый список Ирана</item>
|
<item>Белый список Ирана</item>
|
||||||
|
<item>Белый список России</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="vpn_bypass_lan">
|
<string-array name="vpn_bypass_lan">
|
||||||
|
|||||||
@@ -389,6 +389,7 @@
|
|||||||
<item>黑名单 (Blacklist)</item>
|
<item>黑名单 (Blacklist)</item>
|
||||||
<item>全局 (Global)</item>
|
<item>全局 (Global)</item>
|
||||||
<item>伊朗 (Iran)</item>
|
<item>伊朗 (Iran)</item>
|
||||||
|
<item>俄罗斯白名单 (Russia Whitelist)</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="vpn_bypass_lan">
|
<string-array name="vpn_bypass_lan">
|
||||||
|
|||||||
@@ -389,6 +389,7 @@
|
|||||||
<item>黑名單 (Blacklist)</item>
|
<item>黑名單 (Blacklist)</item>
|
||||||
<item>全域 (Global)</item>
|
<item>全域 (Global)</item>
|
||||||
<item>伊朗 (Iran)</item>
|
<item>伊朗 (Iran)</item>
|
||||||
|
<item>俄羅斯白名單 (Russia Whitelist)</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="vpn_bypass_lan">
|
<string-array name="vpn_bypass_lan">
|
||||||
|
|||||||
@@ -401,6 +401,7 @@
|
|||||||
<item>China Blacklist</item>
|
<item>China Blacklist</item>
|
||||||
<item>Global</item>
|
<item>Global</item>
|
||||||
<item>Iran Whitelist</item>
|
<item>Iran Whitelist</item>
|
||||||
|
<item>Russia Whitelist</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="vpn_bypass_lan">
|
<string-array name="vpn_bypass_lan">
|
||||||
|
|||||||
-1
Submodule hysteria deleted from 44a5643535
@@ -1,20 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
targets=(
|
|
||||||
"aarch64-linux-android24 arm64 arm64-v8a"
|
|
||||||
"armv7a-linux-androideabi24 arm armeabi-v7a"
|
|
||||||
"x86_64-linux-android24 amd64 x86_64"
|
|
||||||
"i686-linux-android24 386 x86"
|
|
||||||
)
|
|
||||||
|
|
||||||
cd "hysteria" || exit
|
|
||||||
|
|
||||||
for target in "${targets[@]}"; do
|
|
||||||
IFS=' ' read -r ndk_target goarch abi <<< "$target"
|
|
||||||
|
|
||||||
echo "Building for ${abi} with ${ndk_target} (${goarch})"
|
|
||||||
|
|
||||||
CC="${NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/${ndk_target}-clang" CGO_ENABLED=1 GOOS=android GOARCH=$goarch go build -o libs/$abi/libhysteria2.so -trimpath -ldflags "-s -w -buildid=" -buildvcs=false ./app
|
|
||||||
|
|
||||||
echo "Built libhysteria2.so for ${abi}"
|
|
||||||
done
|
|
||||||
Reference in New Issue
Block a user