Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f252d1395a | ||
|
|
2dc0472c69 | ||
|
|
3e09adc4d1 | ||
|
|
11750b9382 | ||
|
|
30a4c2199a | ||
|
|
406a9f996e | ||
|
|
5373579bd5 | ||
|
|
9b4cc201e7 | ||
|
|
6f2c96c2b6 | ||
|
|
1f6104de8b | ||
|
|
e078a2ab27 | ||
|
|
5695c17908 | ||
|
|
e2c1081d5a | ||
|
|
bcbcbc91c7 | ||
|
|
5eb3566e8d | ||
|
|
d75eca8dd4 | ||
|
|
640c16d8dc | ||
|
|
1ba5c5a7a6 | ||
|
|
f67af69dda | ||
|
|
5d47777307 | ||
|
|
ab22bb9804 | ||
|
|
2626462e49 | ||
|
|
41893d79c0 | ||
|
|
834c1ba63d | ||
|
|
633ee63891 | ||
|
|
eb5627c0d0 | ||
|
|
6db38f6e3d | ||
|
|
c69a758429 | ||
|
|
cee3a0ffec | ||
|
|
18c0143186 | ||
|
|
bbf0b05b49 | ||
|
|
44723c56ad | ||
|
|
e53c36b53b | ||
|
|
80f26cd4b8 | ||
|
|
b023414cd0 | ||
|
|
2f56104565 | ||
|
|
9cd5fefdca | ||
|
|
25c42c475f | ||
|
|
96e66da071 | ||
|
|
6914b9ee1b | ||
|
|
cfc6546c97 | ||
|
|
0880313659 | ||
|
|
875ca02126 | ||
|
|
c2d5925053 | ||
|
|
547bbf8e95 | ||
|
|
2218251b03 | ||
|
|
4da3a23162 | ||
|
|
28a90baf88 | ||
|
|
7bbdda2f2f | ||
|
|
884b444a41 | ||
|
|
e1ff2df36e | ||
|
|
61c0111778 | ||
|
|
bfc9a64e07 | ||
|
|
2ba92045cc | ||
|
|
aeca9f51c8 | ||
|
|
b107c0ac1d | ||
|
|
65a04b4784 | ||
|
|
eab9f50cfd | ||
|
|
b8bb83b524 | ||
|
|
93eb9fe3b9 | ||
|
|
5f167512f5 | ||
|
|
a727b81263 | ||
|
|
f27c9192d1 | ||
|
|
90153fa17f | ||
|
|
cdfaa01852 | ||
|
|
33d2c3b00d | ||
|
|
b7f992cdc0 | ||
|
|
0a6a24e309 | ||
|
|
153b4cffef | ||
|
|
f09a413232 | ||
|
|
cba58f6ae2 | ||
|
|
2bf4b91488 |
@@ -1,19 +1,20 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.v2ray.ang"
|
||||
compileSdk = 34
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.v2ray.ang"
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = 605
|
||||
versionName = "1.9.11"
|
||||
versionCode = 615
|
||||
versionName = "1.9.19"
|
||||
multiDexEnabled = true
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
@@ -27,20 +28,16 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
|
||||
}
|
||||
debug {
|
||||
isMinifyEnabled = false
|
||||
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +47,13 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
@@ -69,7 +71,8 @@ android {
|
||||
|
||||
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
|
||||
if (versionCodes.containsKey(abi)) {
|
||||
output.versionCodeOverride = (1000000 * versionCodes[abi]!!).plus(variant.versionCode)
|
||||
output.versionCodeOverride =
|
||||
(1000000 * versionCodes[abi]!!).plus(variant.versionCode)
|
||||
} else {
|
||||
return@forEach
|
||||
}
|
||||
@@ -86,49 +89,60 @@ android {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Core Libraries
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.org.mockito.mockito.inline)
|
||||
testImplementation(libs.mockito.kotlin)
|
||||
|
||||
implementation(libs.flexbox)
|
||||
// Androidx
|
||||
implementation(libs.constraintlayout)
|
||||
implementation(libs.legacy.support.v4)
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.cardview)
|
||||
// AndroidX Core Libraries
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.preference.ktx)
|
||||
implementation(libs.recyclerview)
|
||||
implementation(libs.fragment.ktx)
|
||||
implementation(libs.multidex)
|
||||
implementation(libs.viewpager2)
|
||||
|
||||
// Androidx ktx
|
||||
implementation(libs.activity.ktx)
|
||||
// UI Libraries
|
||||
implementation(libs.material)
|
||||
implementation(libs.toastcompat)
|
||||
implementation(libs.editorkit)
|
||||
implementation(libs.flexbox)
|
||||
|
||||
// Data and Storage Libraries
|
||||
implementation(libs.mmkv.static)
|
||||
implementation(libs.gson)
|
||||
|
||||
// Reactive and Utility Libraries
|
||||
implementation(libs.rxjava)
|
||||
implementation(libs.rxandroid)
|
||||
implementation(libs.rxpermissions)
|
||||
|
||||
// Language and Processing Libraries
|
||||
implementation(libs.language.base)
|
||||
implementation(libs.language.json)
|
||||
|
||||
// Intent and Utility Libraries
|
||||
implementation(libs.quickie.bundled)
|
||||
implementation(libs.core)
|
||||
|
||||
// AndroidX Lifecycle and Architecture Components
|
||||
implementation(libs.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.lifecycle.livedata.ktx)
|
||||
implementation(libs.lifecycle.runtime.ktx)
|
||||
|
||||
//kotlin
|
||||
implementation(libs.kotlin.reflect)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
|
||||
implementation(libs.mmkv.static)
|
||||
implementation(libs.gson)
|
||||
implementation(libs.rxjava)
|
||||
implementation(libs.rxandroid)
|
||||
implementation(libs.rxpermissions)
|
||||
implementation(libs.toastcompat)
|
||||
implementation(libs.editorkit)
|
||||
implementation(libs.language.base)
|
||||
implementation(libs.language.json)
|
||||
implementation(libs.quickie.bundled)
|
||||
implementation(libs.core)
|
||||
// Background Task Libraries
|
||||
implementation(libs.work.runtime.ktx)
|
||||
implementation(libs.work.multiprocess)
|
||||
}
|
||||
|
||||
// Multidex Support
|
||||
implementation(libs.multidex)
|
||||
|
||||
// Testing Libraries
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
testImplementation(libs.org.mockito.mockito.inline)
|
||||
testImplementation(libs.mockito.kotlin)
|
||||
}
|
||||
|
||||
BIN
V2rayNG/app/libs/arm64-v8a/libhysteria2.so
Normal file
BIN
V2rayNG/app/libs/arm64-v8a/libhysteria2.so
Normal file
Binary file not shown.
BIN
V2rayNG/app/libs/armeabi-v7a/libhysteria2.so
Normal file
BIN
V2rayNG/app/libs/armeabi-v7a/libhysteria2.so
Normal file
Binary file not shown.
BIN
V2rayNG/app/libs/x86/libhysteria2.so
Normal file
BIN
V2rayNG/app/libs/x86/libhysteria2.so
Normal file
Binary file not shown.
BIN
V2rayNG/app/libs/x86_64/libhysteria2.so
Normal file
BIN
V2rayNG/app/libs/x86_64/libhysteria2.so
Normal file
Binary file not shown.
21
V2rayNG/app/proguard-rules.pro
vendored
21
V2rayNG/app/proguard-rules.pro
vendored
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -41,14 +41,90 @@
|
||||
"geosite:private"
|
||||
]
|
||||
},
|
||||
{
|
||||
"remarks": "代理海外公共DNSIP",
|
||||
"outboundTag": "proxy",
|
||||
"ip": [
|
||||
"1.1.1.1",
|
||||
"1.0.0.1",
|
||||
"2606:4700:4700::1111",
|
||||
"2606:4700:4700::1001",
|
||||
"1.1.1.2",
|
||||
"1.0.0.2",
|
||||
"2606:4700:4700::1112",
|
||||
"2606:4700:4700::1002",
|
||||
"1.1.1.3",
|
||||
"1.0.0.3",
|
||||
"2606:4700:4700::1113",
|
||||
"2606:4700:4700::1003",
|
||||
"8.8.8.8",
|
||||
"8.8.4.4",
|
||||
"2001:4860:4860::8888",
|
||||
"2001:4860:4860::8844",
|
||||
"94.140.14.14",
|
||||
"94.140.15.15",
|
||||
"2a10:50c0::ad1:ff",
|
||||
"2a10:50c0::ad2:ff",
|
||||
"94.140.14.15",
|
||||
"94.140.15.16",
|
||||
"2a10:50c0::bad1:ff",
|
||||
"2a10:50c0::bad2:ff",
|
||||
"94.140.14.140",
|
||||
"94.140.14.141",
|
||||
"2a10:50c0::1:ff",
|
||||
"2a10:50c0::2:ff",
|
||||
"208.67.222.222",
|
||||
"208.67.220.220",
|
||||
"2620:119:35::35",
|
||||
"2620:119:53::53",
|
||||
"208.67.222.123",
|
||||
"208.67.220.123",
|
||||
"2620:119:35::123",
|
||||
"2620:119:53::123",
|
||||
"9.9.9.9",
|
||||
"149.112.112.112",
|
||||
"2620:fe::9",
|
||||
"2620:fe::fe",
|
||||
"9.9.9.11",
|
||||
"149.112.112.11",
|
||||
"2620:fe::11",
|
||||
"2620:fe::fe:11",
|
||||
"9.9.9.10",
|
||||
"149.112.112.10",
|
||||
"2620:fe::10",
|
||||
"2620:fe::fe:10",
|
||||
"77.88.8.8",
|
||||
"77.88.8.1",
|
||||
"2a02:6b8::feed:0ff",
|
||||
"2a02:6b8:0:1::feed:0ff",
|
||||
"77.88.8.88",
|
||||
"77.88.8.2",
|
||||
"2a02:6b8::feed:bad",
|
||||
"2a02:6b8:0:1::feed:bad",
|
||||
"77.88.8.7",
|
||||
"77.88.8.3",
|
||||
"2a02:6b8::feed:a11",
|
||||
"2a02:6b8:0:1::feed:a11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"remarks": "代理海外公共DNS域名",
|
||||
"outboundTag": "proxy",
|
||||
"domain": [
|
||||
"domain:cloudflare-dns.com",
|
||||
"domain:one.one.one.one",
|
||||
"domain:dns.google",
|
||||
"domain:adguard-dns.com",
|
||||
"domain:opendns.com",
|
||||
"domain:umbrella.com",
|
||||
"domain:quad9.net",
|
||||
"domain:yandex.net"
|
||||
]
|
||||
},
|
||||
{
|
||||
"remarks": "代理IP",
|
||||
"outboundTag": "proxy",
|
||||
"ip": [
|
||||
"1.0.0.1",
|
||||
"1.1.1.1",
|
||||
"8.8.8.8",
|
||||
"8.8.4.4",
|
||||
"geoip:facebook",
|
||||
"geoip:fastly",
|
||||
"geoip:google",
|
||||
|
||||
@@ -34,29 +34,62 @@
|
||||
"geosite:private"
|
||||
]
|
||||
},
|
||||
{
|
||||
"remarks": "绕过中国公共DNSIP",
|
||||
"outboundTag": "direct",
|
||||
"ip": [
|
||||
"223.5.5.5",
|
||||
"223.6.6.6",
|
||||
"2400:3200::1",
|
||||
"2400:3200:baba::1",
|
||||
"119.29.29.29",
|
||||
"1.12.12.12",
|
||||
"120.53.53.53",
|
||||
"2402:4e00::",
|
||||
"2402:4e00:1::",
|
||||
"180.76.76.76",
|
||||
"2400:da00::6666",
|
||||
"114.114.114.114",
|
||||
"114.114.115.115",
|
||||
"114.114.114.119",
|
||||
"114.114.115.119",
|
||||
"114.114.114.110",
|
||||
"114.114.115.110",
|
||||
"180.184.1.1",
|
||||
"180.184.2.2",
|
||||
"101.226.4.6",
|
||||
"218.30.118.6",
|
||||
"123.125.81.6",
|
||||
"140.207.198.6",
|
||||
"1.2.4.8",
|
||||
"210.2.4.8",
|
||||
"52.80.66.66",
|
||||
"117.50.22.22",
|
||||
"2400:7fc0:849e:200::4",
|
||||
"2404:c2c0:85d8:901::4",
|
||||
"117.50.10.10",
|
||||
"52.80.52.52",
|
||||
"2400:7fc0:849e:200::8",
|
||||
"2404:c2c0:85d8:901::8",
|
||||
"117.50.60.30",
|
||||
"52.80.60.30"
|
||||
]
|
||||
},
|
||||
{
|
||||
"remarks": "绕过中国公共DNS域名",
|
||||
"outboundTag": "direct",
|
||||
"domain": [
|
||||
"domain:alidns.com",
|
||||
"domain:doh.pub",
|
||||
"domain:dot.pub",
|
||||
"domain:360.cn",
|
||||
"domain:onedns.net"
|
||||
]
|
||||
},
|
||||
{
|
||||
"remarks": "绕过中国IP",
|
||||
"outboundTag": "direct",
|
||||
"ip": [
|
||||
"223.5.5.5/32",
|
||||
"223.6.6.6/32",
|
||||
"2400:3200::1/128",
|
||||
"2400:3200:baba::1/128",
|
||||
"119.29.29.29/32",
|
||||
"1.12.12.12/32",
|
||||
"120.53.53.53/32",
|
||||
"2402:4e00::/128",
|
||||
"2402:4e00:1::/128",
|
||||
"180.76.76.76/32",
|
||||
"2400:da00::6666/128",
|
||||
"114.114.114.114/32",
|
||||
"114.114.115.115/32",
|
||||
"180.184.1.1/32",
|
||||
"180.184.2.2/32",
|
||||
"101.226.4.6/32",
|
||||
"218.30.118.6/32",
|
||||
"123.125.81.6/32",
|
||||
"140.207.198.6/32",
|
||||
"geoip:cn"
|
||||
]
|
||||
},
|
||||
@@ -64,13 +97,7 @@
|
||||
"remarks": "绕过中国域名",
|
||||
"outboundTag": "direct",
|
||||
"domain": [
|
||||
"domain:dns.alidns.com",
|
||||
"domain:doh.pub",
|
||||
"domain:dot.pub",
|
||||
"domain:doh.360.cn",
|
||||
"domain:dot.360.cn",
|
||||
"geosite:cn",
|
||||
"geosite:geolocation-cn"
|
||||
"geosite:cn"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.multidex.MultiDexApplication
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
@@ -22,7 +23,7 @@ class AngApplication : MultiDexApplication() {
|
||||
}
|
||||
|
||||
private val workManagerConfiguration: Configuration = Configuration.Builder()
|
||||
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
|
||||
.setDefaultProcessName("${ANG_PACKAGE}:bg")
|
||||
.build()
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -37,7 +38,7 @@ class AngApplication : MultiDexApplication() {
|
||||
|
||||
MMKV.initialize(this)
|
||||
|
||||
Utils.setNightMode(application)
|
||||
Utils.setNightMode()
|
||||
// Initialize WorkManager with the custom configuration
|
||||
WorkManager.initialize(this, workManagerConfiguration)
|
||||
|
||||
@@ -161,15 +161,20 @@ object AppConfig {
|
||||
const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com"
|
||||
|
||||
// Android Private DNS constants
|
||||
const val DNS_PUB_DOMAIN = "dns.pub"
|
||||
const val DNS_DNSPOD_DOMAIN = "dot.pub"
|
||||
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
|
||||
const val DNS_ONE_ONE_DOMAIN = "one.one.one.one"
|
||||
const val DNS_CLOUDFLARE_DOMAIN = "one.one.one.one"
|
||||
const val DNS_GOOGLE_DOMAIN = "dns.google"
|
||||
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
|
||||
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
|
||||
|
||||
val DNS_PUB_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
||||
|
||||
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
||||
val DNS_ONE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
||||
val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
||||
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
||||
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
|
||||
val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
|
||||
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
|
||||
|
||||
const val DEFAULT_PORT = 443
|
||||
const val DEFAULT_SECURITY = "auto"
|
||||
@@ -8,7 +8,8 @@ enum class Language(val code: String) {
|
||||
VIETNAMESE("vi"),
|
||||
RUSSIAN("ru"),
|
||||
PERSIAN("fa"),
|
||||
BANGLA("bn");
|
||||
BANGLA("bn"),
|
||||
BAKHTIARI("bqi-rIR");
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: String): Language {
|
||||
18
V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt
Normal file
18
V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
enum class NetworkType(val type: String) {
|
||||
TCP("tcp"),
|
||||
KCP("kcp"),
|
||||
WS("ws"),
|
||||
HTTP_UPGRADE("httpupgrade"),
|
||||
SPLIT_HTTP("splithttp"),
|
||||
XHTTP("xhttp"),
|
||||
HTTP("http"),
|
||||
H2("h2"),
|
||||
//QUIC("quic"),
|
||||
GRPC("grpc");
|
||||
|
||||
companion object {
|
||||
fun fromString(type: String?) = entries.find { it.type == type } ?: TCP
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,8 @@ data class ProfileItem(
|
||||
var mode: String? = null,
|
||||
var serviceName: String? = null,
|
||||
var authority: String? = null,
|
||||
var xhttpMode: String? = null,
|
||||
var xhttpExtra: String? = null,
|
||||
|
||||
var security: String? = null,
|
||||
var sni: String? = null,
|
||||
@@ -42,6 +44,7 @@ data class ProfileItem(
|
||||
var spiderX: String? = null,
|
||||
|
||||
var secretKey: String? = null,
|
||||
var preSharedKey: String? = null,
|
||||
var localAddress: String? = null,
|
||||
var reserved: String? = null,
|
||||
var mtu: Int? = null,
|
||||
@@ -66,10 +69,45 @@ data class ProfileItem(
|
||||
return Utils.getIpv6Address(server) + ":" + serverPort
|
||||
}
|
||||
|
||||
fun getKeyProperty(): ProfileItem {
|
||||
val copy = this.copy()
|
||||
copy.subscriptionId = ""
|
||||
copy.addedTime = 0L
|
||||
return copy
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null) return false
|
||||
val obj = other as ProfileItem
|
||||
|
||||
return (this.server == obj.server
|
||||
&& this.serverPort == obj.serverPort
|
||||
&& this.password == obj.password
|
||||
&& this.method == obj.method
|
||||
&& this.flow == obj.flow
|
||||
&& this.username == obj.username
|
||||
|
||||
&& this.network == obj.network
|
||||
&& this.headerType == obj.headerType
|
||||
&& this.host == obj.host
|
||||
&& this.path == obj.path
|
||||
&& this.seed == obj.seed
|
||||
&& this.quicSecurity == obj.quicSecurity
|
||||
&& this.quicKey == obj.quicKey
|
||||
&& this.mode == obj.mode
|
||||
&& this.serviceName == obj.serviceName
|
||||
&& this.authority == obj.authority
|
||||
&& this.xhttpMode == obj.xhttpMode
|
||||
|
||||
&& this.security == obj.security
|
||||
&& this.sni == obj.sni
|
||||
&& this.alpn == obj.alpn
|
||||
&& this.fingerPrint == obj.fingerPrint
|
||||
&& this.publicKey == obj.publicKey
|
||||
&& this.shortId == obj.shortId
|
||||
|
||||
&& this.secretKey == obj.secretKey
|
||||
&& this.localAddress == obj.localAddress
|
||||
&& this.reserved == obj.reserved
|
||||
&& this.mtu == obj.mtu
|
||||
|
||||
&& this.obfsPassword == obj.obfsPassword
|
||||
&& this.portHopping == obj.portHopping
|
||||
&& this.portHoppingInterval == obj.portHoppingInterval
|
||||
&& this.pinSHA256 == obj.pinSHA256
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -195,6 +195,7 @@ data class V2rayConfig(
|
||||
|
||||
data class WireGuardBean(
|
||||
var publicKey: String = "",
|
||||
var preSharedKey: String = "",
|
||||
var endpoint: String = ""
|
||||
)
|
||||
}
|
||||
@@ -206,7 +207,7 @@ data class V2rayConfig(
|
||||
var kcpSettings: KcpSettingsBean? = null,
|
||||
var wsSettings: WsSettingsBean? = null,
|
||||
var httpupgradeSettings: HttpupgradeSettingsBean? = null,
|
||||
var splithttpSettings: SplithttpSettingsBean? = null,
|
||||
var xhttpSettings: XhttpSettingsBean? = null,
|
||||
var httpSettings: HttpSettingsBean? = null,
|
||||
var tlsSettings: TlsSettingsBean? = null,
|
||||
var quicSettings: QuicSettingBean? = null,
|
||||
@@ -275,11 +276,11 @@ data class V2rayConfig(
|
||||
val acceptProxyProtocol: Boolean? = null
|
||||
)
|
||||
|
||||
data class SplithttpSettingsBean(
|
||||
data class XhttpSettingsBean(
|
||||
var path: String? = null,
|
||||
var host: String? = null,
|
||||
val maxUploadSize: Int? = null,
|
||||
val maxConcurrentUploads: Int? = null
|
||||
var mode: String? = null,
|
||||
var extra: Any? = null,
|
||||
)
|
||||
|
||||
data class HttpSettingsBean(
|
||||
@@ -344,14 +345,21 @@ data class V2rayConfig(
|
||||
}
|
||||
|
||||
fun populateTransportSettings(
|
||||
transport: String, headerType: String?, host: String?, path: String?, seed: String?,
|
||||
quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
|
||||
transport: String,
|
||||
headerType: String?,
|
||||
host: String?,
|
||||
path: String?,
|
||||
seed: String?,
|
||||
quicSecurity: String?,
|
||||
key: String?,
|
||||
mode: String?,
|
||||
serviceName: String?,
|
||||
authority: String?
|
||||
): String? {
|
||||
var sni: String? = null
|
||||
network = transport
|
||||
when (network) {
|
||||
"tcp" -> {
|
||||
NetworkType.TCP.type -> {
|
||||
val tcpSetting = TcpSettingsBean()
|
||||
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
|
||||
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
|
||||
@@ -369,7 +377,7 @@ data class V2rayConfig(
|
||||
tcpSettings = tcpSetting
|
||||
}
|
||||
|
||||
"kcp" -> {
|
||||
NetworkType.KCP.type -> {
|
||||
val kcpsetting = KcpSettingsBean()
|
||||
kcpsetting.header.type = headerType ?: "none"
|
||||
if (seed.isNullOrEmpty()) {
|
||||
@@ -380,7 +388,7 @@ data class V2rayConfig(
|
||||
kcpSettings = kcpsetting
|
||||
}
|
||||
|
||||
"ws" -> {
|
||||
NetworkType.WS.type -> {
|
||||
val wssetting = WsSettingsBean()
|
||||
wssetting.headers.Host = host.orEmpty()
|
||||
sni = wssetting.headers.Host
|
||||
@@ -388,7 +396,7 @@ data class V2rayConfig(
|
||||
wsSettings = wssetting
|
||||
}
|
||||
|
||||
"httpupgrade" -> {
|
||||
NetworkType.HTTP_UPGRADE.type -> {
|
||||
val httpupgradeSetting = HttpupgradeSettingsBean()
|
||||
httpupgradeSetting.host = host.orEmpty()
|
||||
sni = httpupgradeSetting.host
|
||||
@@ -396,16 +404,16 @@ data class V2rayConfig(
|
||||
httpupgradeSettings = httpupgradeSetting
|
||||
}
|
||||
|
||||
"splithttp" -> {
|
||||
val splithttpSetting = SplithttpSettingsBean()
|
||||
splithttpSetting.host = host.orEmpty()
|
||||
sni = splithttpSetting.host
|
||||
splithttpSetting.path = path ?: "/"
|
||||
splithttpSettings = splithttpSetting
|
||||
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> {
|
||||
val xhttpSetting = XhttpSettingsBean()
|
||||
xhttpSetting.host = host.orEmpty()
|
||||
sni = xhttpSetting.host
|
||||
xhttpSetting.path = path ?: "/"
|
||||
xhttpSettings = xhttpSetting
|
||||
}
|
||||
|
||||
"h2", "http" -> {
|
||||
network = "h2"
|
||||
NetworkType.H2.type, NetworkType.HTTP.type -> {
|
||||
network = NetworkType.H2.type
|
||||
val h2Setting = HttpSettingsBean()
|
||||
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
sni = h2Setting.host.getOrNull(0) ?: sni
|
||||
@@ -413,15 +421,15 @@ data class V2rayConfig(
|
||||
httpSettings = h2Setting
|
||||
}
|
||||
|
||||
"quic" -> {
|
||||
val quicsetting = QuicSettingBean()
|
||||
quicsetting.security = quicSecurity ?: "none"
|
||||
quicsetting.key = key.orEmpty()
|
||||
quicsetting.header.type = headerType ?: "none"
|
||||
quicSettings = quicsetting
|
||||
}
|
||||
// "quic" -> {
|
||||
// val quicsetting = QuicSettingBean()
|
||||
// quicsetting.security = quicSecurity ?: "none"
|
||||
// quicsetting.key = key.orEmpty()
|
||||
// quicsetting.header.type = headerType ?: "none"
|
||||
// quicSettings = quicsetting
|
||||
// }
|
||||
|
||||
"grpc" -> {
|
||||
NetworkType.GRPC.type -> {
|
||||
val grpcSetting = GrpcSettingsBean()
|
||||
grpcSetting.multiMode = mode == "multi"
|
||||
grpcSetting.serviceName = serviceName.orEmpty()
|
||||
@@ -436,8 +444,14 @@ data class V2rayConfig(
|
||||
}
|
||||
|
||||
fun populateTlsSettings(
|
||||
streamSecurity: String, allowInsecure: Boolean, sni: String?, fingerprint: String?, alpns: String?,
|
||||
publicKey: String?, shortId: String?, spiderX: String?
|
||||
streamSecurity: String,
|
||||
allowInsecure: Boolean,
|
||||
sni: String?,
|
||||
fingerprint: String?,
|
||||
alpns: String?,
|
||||
publicKey: String?,
|
||||
shortId: String?,
|
||||
spiderX: String?
|
||||
) {
|
||||
security = streamSecurity
|
||||
val tlsSetting = TlsSettingsBean(
|
||||
@@ -470,16 +484,16 @@ data class V2rayConfig(
|
||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||
) {
|
||||
return settings?.vnext?.get(0)?.address
|
||||
return settings?.vnext?.first()?.address
|
||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||
) {
|
||||
return settings?.servers?.get(0)?.address
|
||||
return settings?.servers?.first()?.address
|
||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||
return settings?.peers?.get(0)?.endpoint?.substringBeforeLast(":")
|
||||
return settings?.peers?.first()?.endpoint?.substringBeforeLast(":")
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -488,16 +502,16 @@ data class V2rayConfig(
|
||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||
) {
|
||||
return settings?.vnext?.get(0)?.port
|
||||
return settings?.vnext?.first()?.port
|
||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||
) {
|
||||
return settings?.servers?.get(0)?.port
|
||||
return settings?.servers?.first()?.port
|
||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||
return settings?.peers?.get(0)?.endpoint?.substringAfterLast(":")?.toInt()
|
||||
return settings?.peers?.first()?.endpoint?.substringAfterLast(":")?.toInt()
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -512,16 +526,16 @@ data class V2rayConfig(
|
||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||
) {
|
||||
return settings?.vnext?.get(0)?.users?.get(0)?.id
|
||||
return settings?.vnext?.first()?.users?.first()?.id
|
||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||
) {
|
||||
return settings?.servers?.get(0)?.password
|
||||
return settings?.servers?.first()?.password
|
||||
} else if (protocol.equals(EConfigType.SOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||
) {
|
||||
return settings?.servers?.get(0)?.users?.get(0)?.pass
|
||||
return settings?.servers?.first()?.users?.first()?.pass
|
||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||
return settings?.secretKey
|
||||
}
|
||||
@@ -530,9 +544,9 @@ data class V2rayConfig(
|
||||
|
||||
fun getSecurityEncryption(): String? {
|
||||
return when {
|
||||
protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.security
|
||||
protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.encryption
|
||||
protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.get(0)?.method
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -545,7 +559,7 @@ data class V2rayConfig(
|
||||
) {
|
||||
val transport = streamSettings?.network ?: return null
|
||||
return when (transport) {
|
||||
"tcp" -> {
|
||||
NetworkType.TCP.type -> {
|
||||
val tcpSetting = streamSettings?.tcpSettings ?: return null
|
||||
listOf(
|
||||
tcpSetting.header.type,
|
||||
@@ -554,7 +568,7 @@ data class V2rayConfig(
|
||||
)
|
||||
}
|
||||
|
||||
"kcp" -> {
|
||||
NetworkType.KCP.type -> {
|
||||
val kcpSetting = streamSettings?.kcpSettings ?: return null
|
||||
listOf(
|
||||
kcpSetting.header.type,
|
||||
@@ -563,7 +577,7 @@ data class V2rayConfig(
|
||||
)
|
||||
}
|
||||
|
||||
"ws" -> {
|
||||
NetworkType.WS.type -> {
|
||||
val wsSetting = streamSettings?.wsSettings ?: return null
|
||||
listOf(
|
||||
"",
|
||||
@@ -572,7 +586,7 @@ data class V2rayConfig(
|
||||
)
|
||||
}
|
||||
|
||||
"httpupgrade" -> {
|
||||
NetworkType.HTTP_UPGRADE.type -> {
|
||||
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
|
||||
listOf(
|
||||
"",
|
||||
@@ -581,16 +595,16 @@ data class V2rayConfig(
|
||||
)
|
||||
}
|
||||
|
||||
"splithttp" -> {
|
||||
val splithttpSetting = streamSettings?.splithttpSettings ?: return null
|
||||
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> {
|
||||
val xhttpSettings = streamSettings?.xhttpSettings ?: return null
|
||||
listOf(
|
||||
"",
|
||||
splithttpSetting.host,
|
||||
splithttpSetting.path
|
||||
xhttpSettings.host,
|
||||
xhttpSettings.path
|
||||
)
|
||||
}
|
||||
|
||||
"h2" -> {
|
||||
NetworkType.H2.type -> {
|
||||
val h2Setting = streamSettings?.httpSettings ?: return null
|
||||
listOf(
|
||||
"",
|
||||
@@ -599,16 +613,16 @@ data class V2rayConfig(
|
||||
)
|
||||
}
|
||||
|
||||
"quic" -> {
|
||||
val quicSetting = streamSettings?.quicSettings ?: return null
|
||||
listOf(
|
||||
quicSetting.header.type,
|
||||
quicSetting.security,
|
||||
quicSetting.key
|
||||
)
|
||||
}
|
||||
// "quic" -> {
|
||||
// val quicSetting = streamSettings?.quicSettings ?: return null
|
||||
// listOf(
|
||||
// quicSetting.header.type,
|
||||
// quicSetting.security,
|
||||
// quicSetting.key
|
||||
// )
|
||||
// }
|
||||
|
||||
"grpc" -> {
|
||||
NetworkType.GRPC.type -> {
|
||||
val grpcSetting = streamSettings?.grpcSettings ?: return null
|
||||
listOf(
|
||||
if (grpcSetting.multiMode == true) "multi" else "gun",
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.NetworkType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.util.Utils
|
||||
@@ -29,9 +29,40 @@ open class FmtBase {
|
||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||
}
|
||||
|
||||
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>, allowInsecure: Boolean) {
|
||||
config.network = queryParam["type"] ?: NetworkType.TCP.type
|
||||
//TODO
|
||||
if (config.network == NetworkType.SPLIT_HTTP.type) config.network = NetworkType.XHTTP.type
|
||||
config.headerType = queryParam["headerType"]
|
||||
config.host = queryParam["host"]
|
||||
config.path = queryParam["path"]
|
||||
|
||||
config.seed = queryParam["seed"]
|
||||
config.quicSecurity = queryParam["quicSecurity"]
|
||||
config.quicKey = queryParam["key"]
|
||||
config.mode = queryParam["mode"]
|
||||
config.serviceName = queryParam["serviceName"]
|
||||
config.authority = queryParam["authority"]
|
||||
config.xhttpMode = queryParam["mode"]
|
||||
config.xhttpExtra = queryParam["extra"]
|
||||
|
||||
config.security = queryParam["security"]
|
||||
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
|
||||
allowInsecure
|
||||
} else {
|
||||
queryParam["allowInsecure"].orEmpty() == "1"
|
||||
}
|
||||
config.sni = queryParam["sni"]
|
||||
config.fingerPrint = queryParam["fp"]
|
||||
config.alpn = queryParam["alpn"]
|
||||
config.publicKey = queryParam["pbk"]
|
||||
config.shortId = queryParam["sid"]
|
||||
config.spiderX = queryParam["spx"]
|
||||
config.flow = queryParam["flow"]
|
||||
}
|
||||
|
||||
fun getQueryDic(config: ProfileItem): HashMap<String, String> {
|
||||
val dicQuery = HashMap<String, String>()
|
||||
|
||||
dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
|
||||
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
|
||||
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
|
||||
@@ -41,37 +72,45 @@ open class FmtBase {
|
||||
config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() }
|
||||
config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() }
|
||||
|
||||
dicQuery["type"] = config.network?.ifEmpty { AppConfig.DEFAULT_NETWORK }.orEmpty()
|
||||
val networkType = NetworkType.fromString(config.network)
|
||||
dicQuery["type"] = networkType.type
|
||||
|
||||
when (config.network) {
|
||||
"tcp" -> {
|
||||
when (networkType) {
|
||||
NetworkType.TCP -> {
|
||||
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
||||
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
||||
}
|
||||
|
||||
"kcp" -> {
|
||||
NetworkType.KCP -> {
|
||||
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
||||
config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() }
|
||||
}
|
||||
|
||||
"ws", "httpupgrade", "splithttp" -> {
|
||||
NetworkType.WS, NetworkType.HTTP_UPGRADE -> {
|
||||
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
||||
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
||||
}
|
||||
|
||||
"http", "h2" -> {
|
||||
NetworkType.SPLIT_HTTP, NetworkType.XHTTP -> {
|
||||
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
||||
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
||||
config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
|
||||
config.xhttpExtra.let { if (it.isNotNullEmpty()) dicQuery["extra"] = it.orEmpty() }
|
||||
}
|
||||
|
||||
NetworkType.HTTP, NetworkType.H2 -> {
|
||||
dicQuery["type"] = "http"
|
||||
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
||||
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
||||
}
|
||||
|
||||
"quic" -> {
|
||||
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
||||
config.quicSecurity.let { if (it.isNotNullEmpty()) dicQuery["quicSecurity"] = it.orEmpty() }
|
||||
config.quicKey.let { if (it.isNotNullEmpty()) dicQuery["key"] = it.orEmpty() }
|
||||
}
|
||||
// NetworkType.QUIC -> {
|
||||
// dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
||||
// config.quicSecurity.let { if (it.isNotNullEmpty()) dicQuery["quicSecurity"] = it.orEmpty() }
|
||||
// config.quicKey.let { if (it.isNotNullEmpty()) dicQuery["key"] = it.orEmpty() }
|
||||
// }
|
||||
|
||||
"grpc" -> {
|
||||
NetworkType.GRPC -> {
|
||||
config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
|
||||
config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() }
|
||||
config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() }
|
||||
@@ -10,7 +10,7 @@ object HttpFmt : FmtBase() {
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.HTTP)
|
||||
|
||||
outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = profileItem.server.orEmpty()
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
if (profileItem.username.isNotNullEmpty()) {
|
||||
109
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt
Normal file
109
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt
Normal file
@@ -0,0 +1,109 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.NetworkType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
object ShadowsocksFmt : FmtBase() {
|
||||
fun parse(str: String): ProfileItem? {
|
||||
return parseSip002(str) ?: parseLegacy(str)
|
||||
}
|
||||
|
||||
fun parseSip002(str: String): ProfileItem? {
|
||||
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
||||
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
if (uri.idnHost.isEmpty()) return null
|
||||
if (uri.port <= 0) return null
|
||||
if (uri.userInfo.isNullOrEmpty()) return null
|
||||
|
||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||
config.server = uri.idnHost
|
||||
config.serverPort = uri.port.toString()
|
||||
|
||||
val result = if (uri.userInfo.contains(":")) {
|
||||
uri.userInfo.split(":", limit = 2)
|
||||
} else {
|
||||
Utils.decode(uri.userInfo).split(":", limit = 2)
|
||||
}
|
||||
if (result.count() == 2) {
|
||||
config.method = result.first()
|
||||
config.password = result.last()
|
||||
}
|
||||
|
||||
if (!uri.rawQuery.isNullOrEmpty()) {
|
||||
val queryParam = getQueryParam(uri)
|
||||
|
||||
if (queryParam["plugin"] == "obfs-local" && queryParam["obfs"] == "http") {
|
||||
config.network = NetworkType.TCP.type
|
||||
config.headerType = "http"
|
||||
config.host = queryParam["obfs-host"]
|
||||
config.path = queryParam["path"]
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
fun parseLegacy(str: String): ProfileItem? {
|
||||
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
||||
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
|
||||
val indexSplit = result.indexOf("#")
|
||||
if (indexSplit > 0) {
|
||||
try {
|
||||
config.remarks =
|
||||
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
result = result.substring(0, indexSplit)
|
||||
}
|
||||
|
||||
//part decode
|
||||
val indexS = result.indexOf("@")
|
||||
result = if (indexS > 0) {
|
||||
Utils.decode(result.substring(0, indexS)) + result.substring(
|
||||
indexS,
|
||||
result.length
|
||||
)
|
||||
} else {
|
||||
Utils.decode(result)
|
||||
}
|
||||
|
||||
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
|
||||
val match = legacyPattern.matchEntire(result) ?: return null
|
||||
|
||||
config.server = match.groupValues[3].removeSurrounding("[", "]")
|
||||
config.serverPort = match.groupValues[4]
|
||||
config.password = match.groupValues[2]
|
||||
config.method = match.groupValues[1].lowercase()
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
fun toUri(config: ProfileItem): String {
|
||||
val pw = "${config.method}:${config.password}"
|
||||
|
||||
return toUri(config, Utils.encode(pw), null)
|
||||
}
|
||||
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
|
||||
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = profileItem.server.orEmpty()
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
server.password = profileItem.password
|
||||
server.method = profileItem.method
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -15,13 +15,14 @@ object SocksFmt : FmtBase() {
|
||||
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
if (uri.idnHost.isEmpty()) return null
|
||||
if (uri.port <= 0) return null
|
||||
|
||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||
config.server = uri.idnHost
|
||||
config.serverPort = uri.port.toString()
|
||||
|
||||
if (uri.userInfo?.isEmpty() == false) {
|
||||
val result = Utils.decode(uri.userInfo).split(":")
|
||||
val result = Utils.decode(uri.userInfo).split(":", limit = 2)
|
||||
if (result.count() == 2) {
|
||||
config.username = result.first()
|
||||
config.password = result.last()
|
||||
@@ -38,13 +39,13 @@ object SocksFmt : FmtBase() {
|
||||
else
|
||||
":"
|
||||
|
||||
return toUri(config, pw, null)
|
||||
return toUri(config, Utils.encode(pw), null)
|
||||
}
|
||||
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
|
||||
|
||||
outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = profileItem.server.orEmpty()
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
if (profileItem.username.isNotNullEmpty()) {
|
||||
@@ -2,6 +2,7 @@ package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.NetworkType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
@@ -22,37 +23,14 @@ object TrojanFmt : FmtBase() {
|
||||
config.password = uri.userInfo
|
||||
|
||||
if (uri.rawQuery.isNullOrEmpty()) {
|
||||
config.network = NetworkType.TCP.type
|
||||
config.security = AppConfig.TLS
|
||||
config.insecure = allowInsecure
|
||||
|
||||
} else {
|
||||
val queryParam = getQueryParam(uri)
|
||||
|
||||
config.network = queryParam["type"] ?: "tcp"
|
||||
config.headerType = queryParam["headerType"]
|
||||
config.host = queryParam["host"]
|
||||
config.path = queryParam["path"]
|
||||
|
||||
config.seed = queryParam["seed"]
|
||||
config.quicSecurity = queryParam["quicSecurity"]
|
||||
config.quicKey = queryParam["key"]
|
||||
config.mode = queryParam["mode"]
|
||||
config.serviceName = queryParam["serviceName"]
|
||||
config.authority = queryParam["authority"]
|
||||
|
||||
config.security = queryParam["security"]
|
||||
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
|
||||
allowInsecure
|
||||
} else {
|
||||
queryParam["allowInsecure"].orEmpty() == "1"
|
||||
}
|
||||
config.sni = queryParam["sni"]
|
||||
config.fingerPrint = queryParam["fp"]
|
||||
config.alpn = queryParam["alpn"]
|
||||
config.publicKey = queryParam["pbk"]
|
||||
config.shortId = queryParam["sid"]
|
||||
config.spiderX = queryParam["spx"]
|
||||
config.flow = queryParam["flow"]
|
||||
getItemFormQuery(config, queryParam, allowInsecure)
|
||||
config.security = queryParam["security"] ?: AppConfig.TLS
|
||||
}
|
||||
|
||||
return config
|
||||
@@ -67,7 +45,7 @@ object TrojanFmt : FmtBase() {
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
|
||||
|
||||
outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||
server.address = profileItem.server.orEmpty()
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
server.password = profileItem.password
|
||||
@@ -6,6 +6,7 @@ import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
@@ -25,31 +26,7 @@ object VlessFmt : FmtBase() {
|
||||
config.password = uri.userInfo
|
||||
config.method = queryParam["encryption"] ?: "none"
|
||||
|
||||
config.network = queryParam["type"] ?: "tcp"
|
||||
config.headerType = queryParam["headerType"]
|
||||
config.host = queryParam["host"]
|
||||
config.path = queryParam["path"]
|
||||
|
||||
config.seed = queryParam["seed"]
|
||||
config.quicSecurity = queryParam["quicSecurity"]
|
||||
config.quicKey = queryParam["key"]
|
||||
config.mode = queryParam["mode"]
|
||||
config.serviceName = queryParam["serviceName"]
|
||||
config.authority = queryParam["authority"]
|
||||
|
||||
config.security = queryParam["security"]
|
||||
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
|
||||
allowInsecure
|
||||
} else {
|
||||
queryParam["allowInsecure"].orEmpty() == "1"
|
||||
}
|
||||
config.sni = queryParam["sni"]
|
||||
config.fingerPrint = queryParam["fp"]
|
||||
config.alpn = queryParam["alpn"]
|
||||
config.publicKey = queryParam["pbk"]
|
||||
config.shortId = queryParam["sid"]
|
||||
config.spiderX = queryParam["spx"]
|
||||
config.flow = queryParam["flow"]
|
||||
getItemFormQuery(config, queryParam, allowInsecure)
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -65,7 +42,7 @@ object VlessFmt : FmtBase() {
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.VLESS)
|
||||
|
||||
outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||
vnext.address = profileItem.server.orEmpty()
|
||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||
vnext.users[0].id = profileItem.password.orEmpty()
|
||||
@@ -85,6 +62,8 @@ object VlessFmt : FmtBase() {
|
||||
profileItem.serviceName,
|
||||
profileItem.authority,
|
||||
)
|
||||
outboundBean?.streamSettings?.xhttpSettings?.mode = profileItem.xhttpMode
|
||||
outboundBean?.streamSettings?.xhttpSettings?.extra = JsonUtil.parseString(profileItem.xhttpExtra)
|
||||
|
||||
outboundBean?.streamSettings?.populateTlsSettings(
|
||||
profileItem.security.orEmpty(),
|
||||
@@ -4,6 +4,7 @@ import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.NetworkType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.dto.VmessQRCode
|
||||
@@ -47,27 +48,29 @@ object VmessFmt : FmtBase() {
|
||||
config.password = vmessQRCode.id
|
||||
config.method = if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy
|
||||
|
||||
config.network = vmessQRCode.net ?: "tcp"
|
||||
config.network = vmessQRCode.net ?: NetworkType.TCP.type
|
||||
config.headerType = vmessQRCode.type
|
||||
config.host = vmessQRCode.host
|
||||
config.path = vmessQRCode.path
|
||||
|
||||
when (config.network) {
|
||||
"kcp" -> {
|
||||
when (NetworkType.fromString(config.network)) {
|
||||
NetworkType.KCP -> {
|
||||
config.seed = vmessQRCode.path
|
||||
}
|
||||
|
||||
"quic" -> {
|
||||
config.quicSecurity = vmessQRCode.host
|
||||
config.quicKey = vmessQRCode.path
|
||||
}
|
||||
// NetworkType.QUIC -> {
|
||||
// config.quicSecurity = vmessQRCode.host
|
||||
// config.quicKey = vmessQRCode.path
|
||||
// }
|
||||
|
||||
"grpc" -> {
|
||||
NetworkType.GRPC -> {
|
||||
config.mode = vmessQRCode.type
|
||||
config.serviceName = vmessQRCode.path
|
||||
config.authority = vmessQRCode.host
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
config.security = vmessQRCode.tls
|
||||
config.insecure = allowInsecure
|
||||
config.sni = vmessQRCode.sni
|
||||
@@ -90,22 +93,24 @@ object VmessFmt : FmtBase() {
|
||||
|
||||
vmessQRCode.net = config.network.orEmpty()
|
||||
vmessQRCode.type = config.headerType.orEmpty()
|
||||
when (config.network) {
|
||||
"kcp" -> {
|
||||
when (NetworkType.fromString(config.network)) {
|
||||
NetworkType.KCP -> {
|
||||
vmessQRCode.path = config.seed.orEmpty()
|
||||
}
|
||||
|
||||
"quic" -> {
|
||||
vmessQRCode.host = config.quicSecurity.orEmpty()
|
||||
vmessQRCode.path = config.quicKey.orEmpty()
|
||||
}
|
||||
// NetworkType.QUIC -> {
|
||||
// vmessQRCode.host = config.quicSecurity.orEmpty()
|
||||
// vmessQRCode.path = config.quicKey.orEmpty()
|
||||
// }
|
||||
|
||||
"grpc" -> {
|
||||
NetworkType.GRPC -> {
|
||||
vmessQRCode.type = config.mode.orEmpty()
|
||||
vmessQRCode.path = config.serviceName.orEmpty()
|
||||
vmessQRCode.host = config.authority.orEmpty()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
config.host.let { if (it.isNotNullEmpty()) vmessQRCode.host = it.orEmpty() }
|
||||
config.path.let { if (it.isNotNullEmpty()) vmessQRCode.path = it.orEmpty() }
|
||||
|
||||
@@ -119,7 +124,7 @@ object VmessFmt : FmtBase() {
|
||||
}
|
||||
|
||||
fun parseVmessStd(str: String): ProfileItem? {
|
||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||
val config = ProfileItem.create(EConfigType.VMESS)
|
||||
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
@@ -132,23 +137,7 @@ object VmessFmt : FmtBase() {
|
||||
config.password = uri.userInfo
|
||||
config.method = AppConfig.DEFAULT_SECURITY
|
||||
|
||||
config.network = queryParam["type"] ?: "tcp"
|
||||
config.headerType = queryParam["headerType"]
|
||||
config.host = queryParam["host"]
|
||||
config.path = queryParam["path"]
|
||||
|
||||
config.seed = queryParam["seed"]
|
||||
config.quicSecurity = queryParam["quicSecurity"]
|
||||
config.quicKey = queryParam["key"]
|
||||
config.mode = queryParam["mode"]
|
||||
config.serviceName = queryParam["serviceName"]
|
||||
config.authority = queryParam["authority"]
|
||||
|
||||
config.security = queryParam["security"]
|
||||
config.insecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
|
||||
config.sni = queryParam["sni"]
|
||||
config.fingerPrint = queryParam["fp"]
|
||||
config.alpn = queryParam["alpn"]
|
||||
getItemFormQuery(config, queryParam, allowInsecure)
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -157,7 +146,7 @@ object VmessFmt : FmtBase() {
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.VMESS)
|
||||
|
||||
outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||
vnext.address = profileItem.server.orEmpty()
|
||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||
vnext.users[0].id = profileItem.password.orEmpty()
|
||||
@@ -22,9 +22,10 @@ object WireguardFmt : FmtBase() {
|
||||
config.server = uri.idnHost
|
||||
config.serverPort = uri.port.toString()
|
||||
|
||||
config.secretKey = uri.userInfo
|
||||
config.secretKey = uri.userInfo.orEmpty()
|
||||
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
|
||||
config.publicKey = queryParam["publickey"].orEmpty()
|
||||
config.preSharedKey = queryParam["presharedkey"].orEmpty()
|
||||
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||
config.reserved = (queryParam["reserved"] ?: "0,0,0")
|
||||
|
||||
@@ -33,33 +34,75 @@ object WireguardFmt : FmtBase() {
|
||||
|
||||
fun parseWireguardConfFile(str: String): ProfileItem? {
|
||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
||||
val queryParam: MutableMap<String, String> = mutableMapOf()
|
||||
|
||||
val interfaceParams: MutableMap<String, String> = mutableMapOf()
|
||||
val peerParams: MutableMap<String, String> = mutableMapOf()
|
||||
|
||||
var currentSection: String? = null
|
||||
|
||||
str.lines().forEach { line ->
|
||||
val trimmedLine = line.trim()
|
||||
|
||||
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
when {
|
||||
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
|
||||
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
|
||||
trimmedLine.isBlank() || trimmedLine.startsWith("#") -> Unit // Skip blank lines or comments
|
||||
currentSection != null -> {
|
||||
val (key, value) = trimmedLine.split("=").map { it.trim() }
|
||||
queryParam[key.lowercase()] = value // Store the key in lowercase for case-insensitivity
|
||||
else -> {
|
||||
if (currentSection != null) {
|
||||
val parts = trimmedLine.split("=", limit = 2).map { it.trim() }
|
||||
if (parts.size == 2) {
|
||||
val key = parts[0].lowercase()
|
||||
val value = parts[1]
|
||||
when (currentSection) {
|
||||
"Interface" -> interfaceParams[key] = value
|
||||
"Peer" -> peerParams[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.secretKey = queryParam["privatekey"].orEmpty()
|
||||
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
|
||||
config.publicKey = queryParam["publickey"].orEmpty()
|
||||
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||
config.reserved = (queryParam["reserved"] ?: "0,0,0")
|
||||
config.secretKey = interfaceParams["privatekey"].orEmpty()
|
||||
config.remarks = System.currentTimeMillis().toString()
|
||||
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
|
||||
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||
config.publicKey = peerParams["publickey"].orEmpty()
|
||||
config.preSharedKey = peerParams["presharedkey"].orEmpty()
|
||||
val endpoint = peerParams["endpoint"].orEmpty()
|
||||
val endpointParts = endpoint.split(":", limit = 2)
|
||||
if (endpointParts.size == 2) {
|
||||
config.server = endpointParts[0]
|
||||
config.serverPort = endpointParts[1]
|
||||
} else {
|
||||
config.server = endpoint
|
||||
config.serverPort = ""
|
||||
}
|
||||
config.reserved = peerParams["reserved"] ?: "0,0,0"
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
|
||||
|
||||
outboundBean?.settings?.let { wireguard ->
|
||||
wireguard.secretKey = profileItem.secretKey
|
||||
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
||||
wireguard.peers?.firstOrNull()?.let { peer ->
|
||||
peer.publicKey = profileItem.publicKey.orEmpty()
|
||||
peer.preSharedKey = profileItem.preSharedKey.orEmpty()
|
||||
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
||||
}
|
||||
wireguard.mtu = profileItem.mtu
|
||||
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
fun toUri(config: ProfileItem): String {
|
||||
val dicQuery = HashMap<String, String>()
|
||||
@@ -72,24 +115,10 @@ object WireguardFmt : FmtBase() {
|
||||
if (config.mtu != null) {
|
||||
dicQuery["mtu"] = config.mtu.toString()
|
||||
}
|
||||
if (config.preSharedKey != null) {
|
||||
dicQuery["presharedkey"] = Utils.removeWhiteSpace(config.preSharedKey).orEmpty()
|
||||
}
|
||||
|
||||
return toUri(config, config.secretKey, dicQuery)
|
||||
}
|
||||
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
|
||||
|
||||
outboundBean?.settings?.let { wireguard ->
|
||||
wireguard.secretKey = profileItem.secretKey
|
||||
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
||||
wireguard.peers?.get(0)?.publicKey = profileItem.publicKey.orEmpty()
|
||||
wireguard.peers?.get(0)?.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
||||
wireguard.mtu = profileItem.mtu?.toInt()
|
||||
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -277,10 +277,10 @@ object AngConfigManager {
|
||||
if (serverList.isNotEmpty()) {
|
||||
var count = 0
|
||||
for (srv in serverList.reversed()) {
|
||||
val config = CustomFmt.parse(server) ?: continue
|
||||
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
||||
config.subscriptionId = subid
|
||||
val key = MmkvManager.encodeServerConfig("", config)
|
||||
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv))
|
||||
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv)?:"")
|
||||
count += 1
|
||||
}
|
||||
return count
|
||||
@@ -5,6 +5,7 @@ import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.NetworkType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
||||
@@ -19,7 +20,7 @@ object MigrateManager {
|
||||
if (serverStorage.count().toInt() == 0) {
|
||||
return false
|
||||
}
|
||||
var serverList = serverStorage.allKeys() ?: return false
|
||||
val serverList = serverStorage.allKeys() ?: return false
|
||||
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count())
|
||||
|
||||
for (guid in serverList) {
|
||||
@@ -36,7 +37,7 @@ object MigrateManager {
|
||||
|
||||
//check and remove old
|
||||
decodeServerConfig(guid) ?: continue
|
||||
//serverStorage.remove(guid)
|
||||
serverStorage.remove(guid)
|
||||
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + config.remarks)
|
||||
}
|
||||
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end")
|
||||
@@ -70,10 +71,10 @@ object MigrateManager {
|
||||
config.serverPort = outbound.getServerPort().toString()
|
||||
config.method = outbound.getSecurityEncryption()
|
||||
config.password = outbound.getPassword()
|
||||
config.flow = outbound?.settings?.vnext?.get(0)?.users[0]?.flow ?: outbound?.settings?.servers?.get(0)?.flow
|
||||
config.flow = outbound?.settings?.vnext?.first()?.users?.first()?.flow ?: outbound?.settings?.servers?.first()?.flow
|
||||
|
||||
config.network = outbound?.streamSettings?.network ?: NetworkType.TCP.type
|
||||
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
||||
config.network = "tcp"
|
||||
config.headerType = transportDetails[0].orEmpty()
|
||||
config.host = transportDetails[1].orEmpty()
|
||||
config.path = transportDetails[2].orEmpty()
|
||||
@@ -107,7 +108,7 @@ object MigrateManager {
|
||||
config.remarks = configOld.remarks
|
||||
config.server = outbound.getServerAddress()
|
||||
config.serverPort = outbound.getServerPort().toString()
|
||||
config.username = outbound.settings?.servers?.get(0)?.users?.get(0)?.user
|
||||
config.username = outbound.settings?.servers?.first()?.users?.first()?.user
|
||||
config.password = outbound.getPassword()
|
||||
|
||||
return config
|
||||
@@ -120,7 +121,7 @@ object MigrateManager {
|
||||
config.remarks = configOld.remarks
|
||||
config.server = outbound.getServerAddress()
|
||||
config.serverPort = outbound.getServerPort().toString()
|
||||
config.username = outbound.settings?.servers?.get(0)?.users?.get(0)?.user
|
||||
config.username = outbound.settings?.servers?.first()?.users?.first()?.user
|
||||
config.password = outbound.getPassword()
|
||||
|
||||
return config
|
||||
@@ -336,7 +336,7 @@ object MmkvManager {
|
||||
}
|
||||
|
||||
fun decodeSettingsBool(key: String): Boolean {
|
||||
return settingsStorage.decodeBool(key)
|
||||
return settingsStorage.decodeBool(key,false)
|
||||
}
|
||||
|
||||
fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean {
|
||||
@@ -43,12 +43,12 @@ object SettingsManager {
|
||||
}
|
||||
|
||||
|
||||
fun resetRoutingRulesets(context: Context, index: Int) {
|
||||
fun resetRoutingRulesetsFromPresets(context: Context, index: Int) {
|
||||
val rulesetList = getPresetRoutingRulesets(context, index) ?: return
|
||||
resetRoutingRulesetsCommon(rulesetList)
|
||||
}
|
||||
|
||||
fun resetRoutingRulesetsFromClipboard(content: String?): Boolean {
|
||||
fun resetRoutingRulesets(content: String?): Boolean {
|
||||
if (content.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
@@ -95,7 +95,7 @@ object SettingsManager {
|
||||
if (rulesetList.isNullOrEmpty()) return
|
||||
|
||||
if (index < 0 || index >= rulesetList.count()) {
|
||||
rulesetList.add(ruleset)
|
||||
rulesetList.add(0, ruleset)
|
||||
} else {
|
||||
rulesetList[index] = ruleset
|
||||
}
|
||||
@@ -8,12 +8,16 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.AppConfig.DEFAULT_NETWORK
|
||||
import com.v2ray.ang.AppConfig.DNS_ALIDNS_ADDRESSES
|
||||
import com.v2ray.ang.AppConfig.DNS_ALIDNS_DOMAIN
|
||||
import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_ADDRESSES
|
||||
import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_DOMAIN
|
||||
import com.v2ray.ang.AppConfig.DNS_DNSPOD_ADDRESSES
|
||||
import com.v2ray.ang.AppConfig.DNS_DNSPOD_DOMAIN
|
||||
import com.v2ray.ang.AppConfig.DNS_GOOGLE_ADDRESSES
|
||||
import com.v2ray.ang.AppConfig.DNS_GOOGLE_DOMAIN
|
||||
import com.v2ray.ang.AppConfig.DNS_ONE_ONE_ADDRESSES
|
||||
import com.v2ray.ang.AppConfig.DNS_ONE_ONE_DOMAIN
|
||||
import com.v2ray.ang.AppConfig.DNS_PUB_ADDRESSES
|
||||
import com.v2ray.ang.AppConfig.DNS_PUB_DOMAIN
|
||||
import com.v2ray.ang.AppConfig.DNS_QUAD9_ADDRESSES
|
||||
import com.v2ray.ang.AppConfig.DNS_QUAD9_DOMAIN
|
||||
import com.v2ray.ang.AppConfig.DNS_YANDEX_ADDRESSES
|
||||
import com.v2ray.ang.AppConfig.DNS_YANDEX_DOMAIN
|
||||
import com.v2ray.ang.AppConfig.GEOIP_CN
|
||||
import com.v2ray.ang.AppConfig.GEOSITE_CN
|
||||
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
||||
@@ -57,7 +61,7 @@ object V2rayConfigManager {
|
||||
}
|
||||
|
||||
val result = getV2rayNonCustomConfig(context, config)
|
||||
Log.d(ANG_PACKAGE, result.content)
|
||||
//Log.d(ANG_PACKAGE, result.content)
|
||||
result.guid = guid
|
||||
return result
|
||||
} catch (e: Exception) {
|
||||
@@ -376,10 +380,12 @@ object V2rayConfigManager {
|
||||
hosts[GOOGLEAPIS_CN_DOMAIN] = GOOGLEAPIS_COM_DOMAIN
|
||||
|
||||
// hardcode popular Android Private DNS rule to fix localhost DNS problem
|
||||
hosts[DNS_PUB_DOMAIN] = DNS_PUB_ADDRESSES
|
||||
hosts[DNS_ALIDNS_DOMAIN] = DNS_ALIDNS_ADDRESSES
|
||||
hosts[DNS_ONE_ONE_DOMAIN] = DNS_ONE_ONE_ADDRESSES
|
||||
hosts[DNS_CLOUDFLARE_DOMAIN] = DNS_CLOUDFLARE_ADDRESSES
|
||||
hosts[DNS_DNSPOD_DOMAIN] = DNS_DNSPOD_ADDRESSES
|
||||
hosts[DNS_GOOGLE_DOMAIN] = DNS_GOOGLE_ADDRESSES
|
||||
hosts[DNS_QUAD9_DOMAIN] = DNS_QUAD9_ADDRESSES
|
||||
hosts[DNS_YANDEX_DOMAIN] = DNS_YANDEX_ADDRESSES
|
||||
|
||||
|
||||
// DNS dns对象
|
||||
@@ -419,7 +425,7 @@ object V2rayConfigManager {
|
||||
) {
|
||||
muxEnabled = false
|
||||
} else if (protocol.equals(EConfigType.VLESS.name, true)
|
||||
&& outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.isNotEmpty() == true
|
||||
&& outbound.settings?.vnext?.first()?.users?.first()?.flow?.isNotEmpty() == true
|
||||
) {
|
||||
muxEnabled = false
|
||||
}
|
||||
@@ -455,7 +461,7 @@ object V2rayConfigManager {
|
||||
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
|
||||
|
||||
val requestString: String by lazy {
|
||||
"""{"version":"1.1","method":"GET","headers":{"User-Agent":"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"Safari/537.36,"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
|
||||
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
|
||||
}
|
||||
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
|
||||
requestString,
|
||||
@@ -13,46 +13,41 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.v2ray.ang.helper;
|
||||
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
package com.v2ray.ang.helper
|
||||
|
||||
/**
|
||||
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
|
||||
* Interface to listen for a move or dismissal event from a [ItemTouchHelper.Callback].
|
||||
*
|
||||
* @author Paul Burke (ipaulpro)
|
||||
*/
|
||||
public interface ItemTouchHelperAdapter {
|
||||
|
||||
interface ItemTouchHelperAdapter {
|
||||
/**
|
||||
* Called when an item has been dragged far enough to trigger a move. This is called every time
|
||||
* an item is shifted, and <strong>not</strong> at the end of a "drop" event.<br/>
|
||||
* <br/>
|
||||
* Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
|
||||
* an item is shifted, and **not** at the end of a "drop" event.<br></br>
|
||||
* <br></br>
|
||||
* Implementations should call [RecyclerView.Adapter.notifyItemMoved] after
|
||||
* adjusting the underlying data to reflect this move.
|
||||
*
|
||||
* @param fromPosition The start position of the moved item.
|
||||
* @param toPosition Then resolved position of the moved item.
|
||||
* @return True if the item was moved to the new adapter position.
|
||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||
* @see RecyclerView.getAdapterPositionFor
|
||||
* @see RecyclerView.ViewHolder.getAdapterPosition
|
||||
*/
|
||||
boolean onItemMove(int fromPosition, int toPosition);
|
||||
fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
|
||||
|
||||
|
||||
void onItemMoveCompleted();
|
||||
fun onItemMoveCompleted()
|
||||
|
||||
/**
|
||||
* Called when an item has been dismissed by a swipe.<br/>
|
||||
* <br/>
|
||||
* Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
|
||||
* Called when an item has been dismissed by a swipe.<br></br>
|
||||
* <br></br>
|
||||
* Implementations should call [RecyclerView.Adapter.notifyItemRemoved] after
|
||||
* adjusting the underlying data to reflect this removal.
|
||||
*
|
||||
* @param position The position of the item dismissed.
|
||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||
* @see RecyclerView.getAdapterPositionFor
|
||||
* @see RecyclerView.ViewHolder.getAdapterPosition
|
||||
*/
|
||||
void onItemDismiss(int position);
|
||||
fun onItemDismiss(position: Int)
|
||||
}
|
||||
@@ -13,29 +13,26 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.v2ray.ang.helper
|
||||
|
||||
package com.v2ray.ang.helper;
|
||||
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
|
||||
/**
|
||||
* Interface to notify an item ViewHolder of relevant callbacks from {@link
|
||||
* ItemTouchHelper.Callback}.
|
||||
* Interface to notify an item ViewHolder of relevant callbacks from [ ].
|
||||
*
|
||||
* @author Paul Burke (ipaulpro)
|
||||
*/
|
||||
public interface ItemTouchHelperViewHolder {
|
||||
|
||||
interface ItemTouchHelperViewHolder {
|
||||
/**
|
||||
* Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped.
|
||||
* Called when the [ItemTouchHelper] first registers an item as being moved or swiped.
|
||||
* Implementations should update the item view to indicate it's active state.
|
||||
*/
|
||||
void onItemSelected();
|
||||
fun onItemSelected()
|
||||
|
||||
|
||||
/**
|
||||
* Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item
|
||||
* Called when the [ItemTouchHelper] has completed the move or swipe, and the active item
|
||||
* state should be cleared.
|
||||
*/
|
||||
void onItemClear();
|
||||
fun onItemClear()
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Paul Burke
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.v2ray.ang.helper;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.graphics.Canvas;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
|
||||
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br/>
|
||||
* </br/>
|
||||
* Expects the <code>RecyclerView.Adapter</code> to listen for {@link
|
||||
* ItemTouchHelperAdapter} callbacks and the <code>RecyclerView.ViewHolder</code> to implement
|
||||
* {@link ItemTouchHelperViewHolder}.
|
||||
*
|
||||
* @author Paul Burke (ipaulpro)
|
||||
*/
|
||||
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
|
||||
private static final float ALPHA_FULL = 1.0f;
|
||||
private static final float SWIPE_THRESHOLD = 0.25f;
|
||||
private static final long ANIMATION_DURATION = 200;
|
||||
|
||||
private final ItemTouchHelperAdapter mAdapter;
|
||||
private ValueAnimator mReturnAnimator;
|
||||
|
||||
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
|
||||
mAdapter = adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemViewSwipeEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
|
||||
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
||||
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
|
||||
return makeMovementFlags(dragFlags, swipeFlags);
|
||||
} else {
|
||||
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
||||
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
|
||||
return makeMovementFlags(dragFlags, swipeFlags);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder source, @NonNull RecyclerView.ViewHolder target) {
|
||||
if (source.getItemViewType() != target.getItemViewType()) {
|
||||
return false;
|
||||
}
|
||||
mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
// 不执行删除操作,仅返回项目到原位
|
||||
returnViewToOriginalPosition(viewHolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
float dX, float dY, int actionState, boolean isCurrentlyActive) {
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||
float maxSwipeDistance = viewHolder.itemView.getWidth() * SWIPE_THRESHOLD;
|
||||
float swipeAmount = Math.abs(dX);
|
||||
float direction = Math.signum(dX);
|
||||
|
||||
// 限制最大滑动距离
|
||||
float translationX = Math.min(swipeAmount, maxSwipeDistance) * direction;
|
||||
float alpha = ALPHA_FULL - Math.min(swipeAmount, maxSwipeDistance) / maxSwipeDistance;
|
||||
|
||||
viewHolder.itemView.setTranslationX(translationX);
|
||||
viewHolder.itemView.setAlpha(alpha);
|
||||
|
||||
if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
|
||||
returnViewToOriginalPosition(viewHolder);
|
||||
}
|
||||
} else {
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
||||
}
|
||||
}
|
||||
|
||||
private void returnViewToOriginalPosition(RecyclerView.ViewHolder viewHolder) {
|
||||
if (mReturnAnimator != null && mReturnAnimator.isRunning()) {
|
||||
mReturnAnimator.cancel();
|
||||
}
|
||||
|
||||
mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.getTranslationX(), 0f);
|
||||
mReturnAnimator.addUpdateListener(animation -> {
|
||||
float value = (float) animation.getAnimatedValue();
|
||||
viewHolder.itemView.setTranslationX(value);
|
||||
viewHolder.itemView.setAlpha(1f - Math.abs(value) / (viewHolder.itemView.getWidth() * SWIPE_THRESHOLD));
|
||||
});
|
||||
mReturnAnimator.setInterpolator(new DecelerateInterpolator());
|
||||
mReturnAnimator.setDuration(ANIMATION_DURATION);
|
||||
mReturnAnimator.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
|
||||
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
||||
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
||||
itemViewHolder.onItemSelected();
|
||||
}
|
||||
}
|
||||
super.onSelectedChanged(viewHolder, actionState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
viewHolder.itemView.setAlpha(ALPHA_FULL);
|
||||
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
||||
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
||||
itemViewHolder.onItemClear();
|
||||
}
|
||||
mAdapter.onItemMoveCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
return 1.1f; // 设置一个大于1的值,确保不会触发默认的滑动删除操作
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getSwipeEscapeVelocity(float defaultValue) {
|
||||
return defaultValue * 10; // 增加滑动逃逸速度,使得更难触发滑动
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Paul Burke
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.v2ray.ang.helper
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.animation.ValueAnimator.AnimatorUpdateListener
|
||||
import android.graphics.Canvas
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.sign
|
||||
|
||||
/**
|
||||
* An implementation of [ItemTouchHelper.Callback] that enables basic drag & drop and
|
||||
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br></br>
|
||||
*
|
||||
* Expects the `RecyclerView.Adapter` to listen for [ ] callbacks and the `RecyclerView.ViewHolder` to implement
|
||||
* [ItemTouchHelperViewHolder].
|
||||
*
|
||||
* @author Paul Burke (ipaulpro)
|
||||
*/
|
||||
class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() {
|
||||
private var mReturnAnimator: ValueAnimator? = null
|
||||
|
||||
override fun isLongPressDragEnabled(): Boolean = true
|
||||
|
||||
override fun isItemViewSwipeEnabled(): Boolean = true
|
||||
|
||||
override fun getMovementFlags(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
): Int {
|
||||
val dragFlags: Int
|
||||
val swipeFlags: Int
|
||||
if (recyclerView.layoutManager is GridLayoutManager) {
|
||||
dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
|
||||
swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
|
||||
} else {
|
||||
dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
||||
swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
|
||||
}
|
||||
return makeMovementFlags(dragFlags, swipeFlags)
|
||||
}
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
source: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
return if (source.itemViewType != target.itemViewType) {
|
||||
false
|
||||
} else {
|
||||
mAdapter.onItemMove(source.bindingAdapterPosition, target.bindingAdapterPosition)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
// Do not delete; simply return item to original position
|
||||
returnViewToOriginalPosition(viewHolder)
|
||||
}
|
||||
|
||||
override fun onChildDraw(
|
||||
c: Canvas, recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
|
||||
) {
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||
val maxSwipeDistance = viewHolder.itemView.width * SWIPE_THRESHOLD
|
||||
val swipeAmount = abs(dX)
|
||||
val direction = sign(dX)
|
||||
|
||||
// Limit maximum swipe distance
|
||||
val translationX = min(swipeAmount, maxSwipeDistance) * direction
|
||||
val alpha = ALPHA_FULL - min(swipeAmount, maxSwipeDistance) / maxSwipeDistance
|
||||
|
||||
viewHolder.itemView.translationX = translationX
|
||||
viewHolder.itemView.alpha = alpha
|
||||
|
||||
if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
|
||||
returnViewToOriginalPosition(viewHolder)
|
||||
}
|
||||
} else {
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||
}
|
||||
}
|
||||
|
||||
private fun returnViewToOriginalPosition(viewHolder: RecyclerView.ViewHolder) {
|
||||
mReturnAnimator?.takeIf { it.isRunning }?.cancel()
|
||||
|
||||
mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.translationX, 0f).apply {
|
||||
addUpdateListener { animation ->
|
||||
val value = animation.animatedValue as Float
|
||||
viewHolder.itemView.translationX = value
|
||||
viewHolder.itemView.alpha = (1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD))
|
||||
}
|
||||
interpolator = DecelerateInterpolator()
|
||||
duration = ANIMATION_DURATION
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE && viewHolder is ItemTouchHelperViewHolder) {
|
||||
viewHolder.onItemSelected()
|
||||
}
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
}
|
||||
|
||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
viewHolder.itemView.alpha = ALPHA_FULL
|
||||
if (viewHolder is ItemTouchHelperViewHolder) {
|
||||
viewHolder.onItemClear()
|
||||
}
|
||||
mAdapter.onItemMoveCompleted()
|
||||
}
|
||||
|
||||
override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
|
||||
return 1.1f // Set a value greater than 1 to prevent default swipe delete
|
||||
}
|
||||
|
||||
override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
|
||||
return defaultValue * 10 // Increase swipe escape velocity to make swipe harder to trigger
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ALPHA_FULL = 1.0f
|
||||
private const val SWIPE_THRESHOLD = 0.25f
|
||||
private const val ANIMATION_DURATION: Long = 200
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.v2ray.ang.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
//Check if context is not null and action is the one we want
|
||||
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return
|
||||
//Check if flag is true and a server is selected
|
||||
if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
|
||||
//Start v2ray
|
||||
V2RayServiceManager.startV2Ray(context)
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,9 @@ class TaskerReceiver : BroadcastReceiver() {
|
||||
try {
|
||||
val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE)
|
||||
val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false)
|
||||
val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "")
|
||||
val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID).orEmpty()
|
||||
|
||||
if (switch == null || guid == null || TextUtils.isEmpty(guid)) {
|
||||
if (switch == null || TextUtils.isEmpty(guid)) {
|
||||
return
|
||||
} else if (switch) {
|
||||
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
|
||||
@@ -8,11 +8,10 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ProcessService {
|
||||
private val TAG = ANG_PACKAGE
|
||||
private var process: Process? = null
|
||||
|
||||
fun runProcess(context: Context, cmd: MutableList<String>) {
|
||||
Log.d(TAG, cmd.toString())
|
||||
Log.d(ANG_PACKAGE, cmd.toString())
|
||||
|
||||
try {
|
||||
val proBuilder = ProcessBuilder(cmd)
|
||||
@@ -23,23 +22,23 @@ class ProcessService {
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
Thread.sleep(50L)
|
||||
Log.d(TAG, "runProcess check")
|
||||
Log.d(ANG_PACKAGE, "runProcess check")
|
||||
process?.waitFor()
|
||||
Log.d(TAG, "runProcess exited")
|
||||
Log.d(ANG_PACKAGE, "runProcess exited")
|
||||
}
|
||||
Log.d(TAG, process.toString())
|
||||
Log.d(ANG_PACKAGE, process.toString())
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, e.toString())
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun stopProcess() {
|
||||
try {
|
||||
Log.d(TAG, "runProcess destroy")
|
||||
Log.d(ANG_PACKAGE, "runProcess destroy")
|
||||
process?.destroy()
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, e.toString())
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
@@ -32,24 +33,31 @@ class QSTileService : TileService() {
|
||||
qsTile?.updateTile()
|
||||
}
|
||||
|
||||
/**
|
||||
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
||||
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||
*/
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
|
||||
setState(Tile.STATE_INACTIVE)
|
||||
mMsgReceive = ReceiveMessageHandler(this)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY), RECEIVER_EXPORTED)
|
||||
} else {
|
||||
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
|
||||
}
|
||||
|
||||
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
||||
ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags())
|
||||
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
super.onStopListening()
|
||||
|
||||
unregisterReceiver(mMsgReceive)
|
||||
mMsgReceive = null
|
||||
try {
|
||||
applicationContext.unregisterReceiver(mMsgReceive)
|
||||
mMsgReceive = null
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
@@ -13,6 +13,7 @@ import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||
@@ -126,6 +127,11 @@ object V2RayServiceManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
||||
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||
*/
|
||||
|
||||
fun startV2rayPoint() {
|
||||
val service = serviceControl?.get()?.getService() ?: return
|
||||
val guid = MmkvManager.getSelectServer() ?: return
|
||||
@@ -142,11 +148,7 @@ object V2RayServiceManager {
|
||||
mFilter.addAction(Intent.ACTION_SCREEN_ON)
|
||||
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
service.registerReceiver(mMsgReceive, mFilter, Context.RECEIVER_EXPORTED)
|
||||
} else {
|
||||
service.registerReceiver(mMsgReceive, mFilter)
|
||||
}
|
||||
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
@@ -156,7 +158,7 @@ object V2RayServiceManager {
|
||||
currentConfig = config
|
||||
|
||||
try {
|
||||
v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true)
|
||||
v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import libv2ray.Libv2ray
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class V2RayTestService : Service() {
|
||||
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(10).asCoroutineDispatcher()) }
|
||||
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -17,6 +17,7 @@ import android.os.StrictMode
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||
import com.v2ray.ang.BuildConfig
|
||||
import com.v2ray.ang.R
|
||||
@@ -153,9 +154,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
|
||||
|
||||
val selfPackageName = BuildConfig.APPLICATION_ID
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY) == true) {
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)) {
|
||||
val apps = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||
val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS) == true
|
||||
val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS)
|
||||
//process self package
|
||||
if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName)
|
||||
apps?.forEach {
|
||||
@@ -165,6 +166,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
else
|
||||
builder.addAllowedApplication(it)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Log.d(ANG_PACKAGE, "setup error : --${e.localizedMessage}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -215,11 +217,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
"--loglevel", "notice"
|
||||
)
|
||||
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) {
|
||||
cmd.add("--netif-ip6addr")
|
||||
cmd.add(PRIVATE_VLAN6_ROUTER)
|
||||
}
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) {
|
||||
val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
|
||||
cmd.add("--dnsgw")
|
||||
cmd.add("$LOOPBACK:${localDnsPort}")
|
||||
@@ -232,7 +234,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
process = proBuilder
|
||||
.directory(applicationContext.filesDir)
|
||||
.start()
|
||||
Thread(Runnable {
|
||||
Thread {
|
||||
Log.d(packageName, "$TUN2SOCKS check")
|
||||
process.waitFor()
|
||||
Log.d(packageName, "$TUN2SOCKS exited")
|
||||
@@ -240,7 +242,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
Log.d(packageName, "$TUN2SOCKS restart")
|
||||
runTun2socks()
|
||||
}
|
||||
}).start()
|
||||
}.start()
|
||||
Log.d(packageName, process.toString())
|
||||
|
||||
sendFd()
|
||||
@@ -133,7 +133,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
.request(Manifest.permission.POST_NOTIFICATIONS)
|
||||
.subscribe {
|
||||
if (!it)
|
||||
toast(R.string.toast_permission_denied)
|
||||
toast(R.string.toast_permission_denied_notification)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
|
||||
@@ -83,13 +86,13 @@ class RoutingSettingActivity : BaseActivity() {
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_rulesets -> {
|
||||
R.id.import_predefined_rulesets -> {
|
||||
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
|
||||
try {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
SettingsManager.resetRoutingRulesets(this@RoutingSettingActivity, i)
|
||||
SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i)
|
||||
launch(Dispatchers.Main) {
|
||||
refreshData()
|
||||
toast(R.string.toast_success)
|
||||
@@ -120,7 +123,7 @@ class RoutingSettingActivity : BaseActivity() {
|
||||
return@setPositiveButton
|
||||
}
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val result = SettingsManager.resetRoutingRulesetsFromClipboard(clipboard)
|
||||
val result = SettingsManager.resetRoutingRulesets(clipboard)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (result) {
|
||||
refreshData()
|
||||
@@ -138,6 +141,18 @@ class RoutingSettingActivity : BaseActivity() {
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_rulesets_from_qrcode -> {
|
||||
RxPermissions(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.subscribe {
|
||||
if (it)
|
||||
scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java))
|
||||
else
|
||||
toast(R.string.toast_permission_denied)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
R.id.export_rulesets_to_clipboard -> {
|
||||
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
||||
@@ -153,6 +168,34 @@ class RoutingSettingActivity : BaseActivity() {
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private val scanQRcodeForRulesets = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
importRulesetsFromQRcode(it.data?.getStringExtra("SCAN_RESULT"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun importRulesetsFromQRcode(qrcode: String?): Boolean {
|
||||
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val result = SettingsManager.resetRoutingRulesets(qrcode)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (result) {
|
||||
refreshData()
|
||||
toast(R.string.toast_success)
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||
//do nothing
|
||||
}
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
|
||||
fun refreshData() {
|
||||
rulesets.clear()
|
||||
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
|
||||
@@ -24,7 +24,9 @@ import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
|
||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.NetworkType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
@@ -79,6 +81,10 @@ class ServerActivity : BaseActivity() {
|
||||
private val alpns: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.streamsecurity_alpn)
|
||||
}
|
||||
private val xhttpMode: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.xhttp_mode)
|
||||
}
|
||||
|
||||
|
||||
// Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
|
||||
// We don't use AndroidViewBinding because, it is better to share similar logics for different
|
||||
@@ -107,6 +113,7 @@ class ServerActivity : BaseActivity() {
|
||||
private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
|
||||
private val container_alpn: LinearLayout? by lazy { findViewById(R.id.lay_stream_alpn) }
|
||||
private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) }
|
||||
private val et_preshared_key: EditText? by lazy { findViewById(R.id.et_preshared_key) }
|
||||
private val container_public_key: LinearLayout? by lazy { findViewById(R.id.lay_public_key) }
|
||||
private val et_short_id: EditText? by lazy { findViewById(R.id.et_short_id) }
|
||||
private val container_short_id: LinearLayout? by lazy { findViewById(R.id.lay_short_id) }
|
||||
@@ -119,6 +126,8 @@ class ServerActivity : BaseActivity() {
|
||||
private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) }
|
||||
private val et_port_hop_interval: EditText? by lazy { findViewById(R.id.et_port_hop_interval) }
|
||||
private val et_pinsha256: EditText? by lazy { findViewById(R.id.et_pinsha256) }
|
||||
private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
|
||||
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -142,7 +151,7 @@ class ServerActivity : BaseActivity() {
|
||||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long
|
||||
id: Long,
|
||||
) {
|
||||
val types = transportTypes(networks[position])
|
||||
sp_header_type?.isEnabled = types.size > 1
|
||||
@@ -150,28 +159,49 @@ class ServerActivity : BaseActivity() {
|
||||
ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
sp_header_type?.adapter = adapter
|
||||
sp_header_type_title?.text = if (networks[position] == "grpc")
|
||||
getString(R.string.server_lab_mode_type) else
|
||||
getString(R.string.server_lab_head_type)
|
||||
config?.headerType?.let { it ->
|
||||
sp_header_type?.setSelection(Utils.arrayFind(types, it))
|
||||
}
|
||||
config?.host?.let { it ->
|
||||
et_request_host?.text = Utils.getEditable(it)
|
||||
}
|
||||
config?.path?.let { it ->
|
||||
et_path?.text = Utils.getEditable(it)
|
||||
}
|
||||
sp_header_type_title?.text =
|
||||
when (networks[position]) {
|
||||
NetworkType.GRPC.type -> getString(R.string.server_lab_mode_type)
|
||||
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> getString(R.string.server_lab_xhttp_mode)
|
||||
else -> getString(R.string.server_lab_head_type)
|
||||
}.orEmpty()
|
||||
sp_header_type?.setSelection(
|
||||
Utils.arrayFind(
|
||||
types,
|
||||
when (networks[position]) {
|
||||
NetworkType.GRPC.type -> config?.mode
|
||||
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> config?.xhttpMode
|
||||
else -> config?.headerType
|
||||
}.orEmpty()
|
||||
)
|
||||
)
|
||||
|
||||
et_request_host?.text = Utils.getEditable(
|
||||
when (networks[position]) {
|
||||
//"quic" -> config?.quicSecurity
|
||||
NetworkType.GRPC.type -> config?.authority
|
||||
else -> config?.host
|
||||
}.orEmpty()
|
||||
)
|
||||
et_path?.text = Utils.getEditable(
|
||||
when (networks[position]) {
|
||||
NetworkType.KCP.type -> config?.seed
|
||||
//"quic" -> config?.quicKey
|
||||
NetworkType.GRPC.type -> config?.serviceName
|
||||
else -> config?.path
|
||||
}.orEmpty()
|
||||
)
|
||||
|
||||
tv_request_host?.text = Utils.getEditable(
|
||||
getString(
|
||||
when (networks[position]) {
|
||||
"tcp" -> R.string.server_lab_request_host_http
|
||||
"ws" -> R.string.server_lab_request_host_ws
|
||||
"httpupgrade" -> R.string.server_lab_request_host_httpupgrade
|
||||
"splithttp" -> R.string.server_lab_request_host_splithttp
|
||||
"h2" -> R.string.server_lab_request_host_h2
|
||||
"quic" -> R.string.server_lab_request_host_quic
|
||||
"grpc" -> R.string.server_lab_request_host_grpc
|
||||
NetworkType.TCP.type -> R.string.server_lab_request_host_http
|
||||
NetworkType.WS.type -> R.string.server_lab_request_host_ws
|
||||
NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_request_host_httpupgrade
|
||||
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> R.string.server_lab_request_host_xhttp
|
||||
NetworkType.H2.type -> R.string.server_lab_request_host_h2
|
||||
//"quic" -> R.string.server_lab_request_host_quic
|
||||
NetworkType.GRPC.type -> R.string.server_lab_request_host_grpc
|
||||
else -> R.string.server_lab_request_host
|
||||
}
|
||||
)
|
||||
@@ -180,17 +210,29 @@ class ServerActivity : BaseActivity() {
|
||||
tv_path?.text = Utils.getEditable(
|
||||
getString(
|
||||
when (networks[position]) {
|
||||
"kcp" -> R.string.server_lab_path_kcp
|
||||
"ws" -> R.string.server_lab_path_ws
|
||||
"httpupgrade" -> R.string.server_lab_path_httpupgrade
|
||||
"splithttp" -> R.string.server_lab_path_splithttp
|
||||
"h2" -> R.string.server_lab_path_h2
|
||||
"quic" -> R.string.server_lab_path_quic
|
||||
"grpc" -> R.string.server_lab_path_grpc
|
||||
NetworkType.KCP.type -> R.string.server_lab_path_kcp
|
||||
NetworkType.WS.type -> R.string.server_lab_path_ws
|
||||
NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_path_httpupgrade
|
||||
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> R.string.server_lab_path_xhttp
|
||||
NetworkType.H2.type -> R.string.server_lab_path_h2
|
||||
//"quic" -> R.string.server_lab_path_quic
|
||||
NetworkType.GRPC.type -> R.string.server_lab_path_grpc
|
||||
else -> R.string.server_lab_path
|
||||
}
|
||||
)
|
||||
)
|
||||
et_extra?.text = Utils.getEditable(
|
||||
when (networks[position]) {
|
||||
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> config?.xhttpExtra
|
||||
else -> null
|
||||
}.orEmpty()
|
||||
)
|
||||
|
||||
layout_extra?.visibility =
|
||||
when (networks[position]) {
|
||||
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
@@ -202,7 +244,7 @@ class ServerActivity : BaseActivity() {
|
||||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long
|
||||
id: Long,
|
||||
) {
|
||||
val isBlank = streamSecuritys[position].isBlank()
|
||||
val isTLS = streamSecuritys[position] == TLS
|
||||
@@ -280,29 +322,21 @@ class ServerActivity : BaseActivity() {
|
||||
} else if (config.configType == EConfigType.WIREGUARD) {
|
||||
et_id.text = Utils.getEditable(config.secretKey.orEmpty())
|
||||
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
|
||||
if (config.reserved == null) {
|
||||
et_reserved1?.text = Utils.getEditable("0,0,0")
|
||||
} else {
|
||||
et_reserved1?.text = Utils.getEditable(config.reserved?.toString())
|
||||
}
|
||||
if (config.localAddress == null) {
|
||||
et_local_address?.text = Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
|
||||
} else {
|
||||
et_local_address?.text = Utils.getEditable(config.localAddress)
|
||||
}
|
||||
if (config.mtu == null) {
|
||||
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
|
||||
} else {
|
||||
et_local_mtu?.text = Utils.getEditable(config.mtu.toString())
|
||||
}
|
||||
et_preshared_key?.visibility = View.VISIBLE
|
||||
et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
|
||||
et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
|
||||
et_local_address?.text = Utils.getEditable(
|
||||
config.localAddress ?: "$WIREGUARD_LOCAL_ADDRESS_V4,$WIREGUARD_LOCAL_ADDRESS_V6"
|
||||
)
|
||||
et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU)
|
||||
} else if (config.configType == EConfigType.HYSTERIA2) {
|
||||
et_obfs_password?.text = Utils.getEditable(config.obfsPassword)
|
||||
et_port_hop?.text = Utils.getEditable(config.portHopping)
|
||||
et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval)
|
||||
et_pinsha256?.text = Utils.getEditable(config.pinSHA256)
|
||||
}
|
||||
|
||||
val securityEncryptions = if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
||||
val securityEncryptions =
|
||||
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
||||
val security = Utils.arrayFind(securityEncryptions, config.method.orEmpty())
|
||||
if (security >= 0) {
|
||||
sp_security?.setSelection(security)
|
||||
@@ -333,7 +367,7 @@ class ServerActivity : BaseActivity() {
|
||||
container_public_key?.visibility = View.GONE
|
||||
container_short_id?.visibility = View.GONE
|
||||
container_spider_x?.visibility = View.GONE
|
||||
} else if (config.security == REALITY) { // reality settings
|
||||
} else if (config.security == REALITY) {
|
||||
container_public_key?.visibility = View.VISIBLE
|
||||
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
|
||||
container_short_id?.visibility = View.VISIBLE
|
||||
@@ -353,7 +387,6 @@ class ServerActivity : BaseActivity() {
|
||||
container_short_id?.visibility = View.GONE
|
||||
container_spider_x?.visibility = View.GONE
|
||||
}
|
||||
|
||||
val network = Utils.arrayFind(networks, config.network.orEmpty())
|
||||
if (network >= 0) {
|
||||
sp_network?.setSelection(network)
|
||||
@@ -407,7 +440,8 @@ class ServerActivity : BaseActivity() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(createConfigType)
|
||||
val config =
|
||||
MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(createConfigType)
|
||||
if (config.configType != EConfigType.SOCKS
|
||||
&& config.configType != EConfigType.HTTP
|
||||
&& TextUtils.isEmpty(et_id.text.toString())
|
||||
@@ -428,6 +462,12 @@ class ServerActivity : BaseActivity() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (et_extra?.text?.toString().isNotNullEmpty()) {
|
||||
if (JsonUtil.parseString(et_extra?.text?.toString()) == null) {
|
||||
toast(R.string.server_lab_xhttp_extra)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
saveCommon(config)
|
||||
saveStreamSettings(config)
|
||||
@@ -436,7 +476,7 @@ class ServerActivity : BaseActivity() {
|
||||
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
|
||||
config.subscriptionId = subscriptionId.orEmpty()
|
||||
}
|
||||
Log.d(ANG_PACKAGE, JsonUtil.toJsonPretty(config))
|
||||
Log.d(ANG_PACKAGE, JsonUtil.toJsonPretty(config) ?: "")
|
||||
MmkvManager.encodeServerConfig(editGuid, config)
|
||||
toast(R.string.toast_success)
|
||||
finish()
|
||||
@@ -464,6 +504,7 @@ class ServerActivity : BaseActivity() {
|
||||
} else if (config.configType == EConfigType.WIREGUARD) {
|
||||
config.secretKey = et_id.text.toString().trim()
|
||||
config.publicKey = et_public_key?.text.toString().trim()
|
||||
config.preSharedKey = et_preshared_key?.text.toString().trim()
|
||||
config.reserved = et_reserved1?.text.toString().trim()
|
||||
config.localAddress = et_local_address?.text.toString().trim()
|
||||
config.mtu = Utils.parseInt(et_local_mtu?.text.toString())
|
||||
@@ -492,6 +533,8 @@ class ServerActivity : BaseActivity() {
|
||||
profileItem.mode = transportTypes(networks[network])[type]
|
||||
profileItem.serviceName = path
|
||||
profileItem.authority = requestHost
|
||||
profileItem.xhttpMode = transportTypes(networks[network])[type]
|
||||
profileItem.xhttpExtra = et_extra?.text?.toString()?.trim()
|
||||
}
|
||||
|
||||
private fun saveTls(config: ProfileItem) {
|
||||
@@ -506,7 +549,7 @@ class ServerActivity : BaseActivity() {
|
||||
|
||||
val allowInsecure =
|
||||
if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
|
||||
MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE) == true
|
||||
MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE)
|
||||
} else {
|
||||
allowinsecures[allowInsecureField].toBoolean()
|
||||
}
|
||||
@@ -523,18 +566,22 @@ class ServerActivity : BaseActivity() {
|
||||
|
||||
private fun transportTypes(network: String?): Array<out String> {
|
||||
return when (network) {
|
||||
"tcp" -> {
|
||||
NetworkType.TCP.type -> {
|
||||
tcpTypes
|
||||
}
|
||||
|
||||
"kcp", "quic" -> {
|
||||
NetworkType.KCP.type -> {
|
||||
kcpAndQuicTypes
|
||||
}
|
||||
|
||||
"grpc" -> {
|
||||
NetworkType.GRPC.type -> {
|
||||
grpcModes
|
||||
}
|
||||
|
||||
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> {
|
||||
xhttpMode
|
||||
}
|
||||
|
||||
else -> {
|
||||
arrayOf("---")
|
||||
}
|
||||
@@ -12,10 +12,9 @@ import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.fmt.CustomFmt
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import me.drakeet.support.toast.ToastCompat
|
||||
|
||||
@@ -75,8 +74,8 @@ class ServerCustomConfigActivity : BaseActivity() {
|
||||
return false
|
||||
}
|
||||
|
||||
val v2rayConfig = try {
|
||||
JsonUtil.fromJson(binding.editor.text.toString(), V2rayConfig::class.java)
|
||||
val profileItem = try {
|
||||
CustomFmt.parse(binding.editor.text.toString())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
|
||||
@@ -84,7 +83,11 @@ class ServerCustomConfigActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(EConfigType.CUSTOM)
|
||||
config.remarks = if (binding.etRemarks.text.isNullOrEmpty()) v2rayConfig.remarks.orEmpty() else binding.etRemarks.text.toString()
|
||||
binding.etRemarks.text.let {
|
||||
config.remarks = if (it.isNullOrEmpty()) profileItem?.remarks.orEmpty() else it.toString()
|
||||
}
|
||||
config.server = profileItem?.server
|
||||
config.serverPort = profileItem?.serverPort
|
||||
|
||||
MmkvManager.encodeServerConfig(editGuid, config)
|
||||
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())
|
||||
@@ -75,6 +75,7 @@ class UserAssetActivity : BaseActivity() {
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.add_file -> showFileChooser().let { true }
|
||||
R.id.add_url -> startActivity(Intent(this, UserAssetUrlActivity::class.java)).let { true }
|
||||
R.id.add_qrcode -> importAssetFromQRcode().let { true }
|
||||
R.id.download_file -> downloadGeoFiles().let { true }
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
@@ -156,6 +157,40 @@ class UserAssetActivity : BaseActivity() {
|
||||
null
|
||||
}
|
||||
|
||||
private fun importAssetFromQRcode(): Boolean {
|
||||
RxPermissions(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.subscribe {
|
||||
if (it)
|
||||
scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java))
|
||||
else
|
||||
toast(R.string.toast_permission_denied)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private val scanQRCodeForAssetURL = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
importAsset(it.data?.getStringExtra("SCAN_RESULT"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun importAsset(url: String?): Boolean {
|
||||
try {
|
||||
if (!Utils.isValidUrl(url)) {
|
||||
toast(R.string.toast_invalid_url)
|
||||
return false
|
||||
}
|
||||
// Send URL to UserAssetUrlActivity for Processing
|
||||
startActivity(Intent(this, UserAssetUrlActivity::class.java)
|
||||
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun downloadGeoFiles() {
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
||||
@@ -14,6 +14,11 @@ import com.v2ray.ang.util.Utils
|
||||
import java.io.File
|
||||
|
||||
class UserAssetUrlActivity : BaseActivity() {
|
||||
// Receive QRcode URL from UserAssetActivity
|
||||
companion object {
|
||||
const val ASSET_URL_QRCODE = "ASSET_URL_QRCODE"
|
||||
}
|
||||
|
||||
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
|
||||
|
||||
var del_config: MenuItem? = null
|
||||
@@ -28,10 +33,15 @@ class UserAssetUrlActivity : BaseActivity() {
|
||||
title = getString(R.string.title_user_asset_add_url)
|
||||
|
||||
val assetItem = MmkvManager.decodeAsset(editAssetId)
|
||||
if (assetItem != null) {
|
||||
bindingAsset(assetItem)
|
||||
} else {
|
||||
clearAsset()
|
||||
val assetUrlQrcode = intent.getStringExtra(ASSET_URL_QRCODE)
|
||||
val assetNameQrcode = File(assetUrlQrcode.toString()).name
|
||||
when {
|
||||
assetItem != null -> bindingAsset(assetItem)
|
||||
assetUrlQrcode != null -> {
|
||||
binding.etRemarks.setText(assetNameQrcode)
|
||||
binding.etUrl.setText(assetUrlQrcode)
|
||||
}
|
||||
else -> clearAsset()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,11 @@ object AppManagerUtil {
|
||||
val apps = ArrayList<AppInfo>()
|
||||
|
||||
for (pkg in packages) {
|
||||
//if (!pkg.hasInternetPermission && pkg.packageName != "android") continue
|
||||
|
||||
val applicationInfo = pkg.applicationInfo
|
||||
val applicationInfo = pkg.applicationInfo ?: continue
|
||||
|
||||
val appName = applicationInfo.loadLabel(packageManager).toString()
|
||||
val appIcon = applicationInfo.loadIcon(packageManager)
|
||||
val isSystemApp = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM > 0
|
||||
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
|
||||
val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
|
||||
|
||||
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
|
||||
apps.add(appInfo)
|
||||
@@ -2,6 +2,8 @@ package com.v2ray.ang.util
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import com.google.gson.JsonPrimitive
|
||||
import com.google.gson.JsonSerializationContext
|
||||
import com.google.gson.JsonSerializer
|
||||
@@ -15,11 +17,13 @@ object JsonUtil {
|
||||
return gson.toJson(src)
|
||||
}
|
||||
|
||||
fun <T> fromJson(json: String, cls: Class<T>): T {
|
||||
return gson.fromJson(json, cls)
|
||||
fun <T> fromJson(src: String, cls: Class<T>): T {
|
||||
return gson.fromJson(src, cls)
|
||||
}
|
||||
|
||||
fun toJsonPretty(src: Any?): String {
|
||||
fun toJsonPretty(src: Any?): String? {
|
||||
if (src == null)
|
||||
return null
|
||||
val gsonPre = GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.disableHtmlEscaping()
|
||||
@@ -34,4 +38,15 @@ object JsonUtil {
|
||||
.create()
|
||||
return gsonPre.toJson(src)
|
||||
}
|
||||
|
||||
fun parseString(src: String?): JsonObject? {
|
||||
if (src == null)
|
||||
return null
|
||||
try {
|
||||
return JsonParser.parseString(src).getAsJsonObject()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import android.util.Log
|
||||
import android.util.Patterns
|
||||
import android.webkit.URLUtil
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||
@@ -130,7 +131,8 @@ object Utils {
|
||||
* get remote dns servers from preference
|
||||
*/
|
||||
fun getRemoteDnsServers(): List<String> {
|
||||
val remoteDns = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
|
||||
val remoteDns =
|
||||
MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
|
||||
val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
|
||||
if (ret.isEmpty()) {
|
||||
return listOf(AppConfig.DNS_PROXY)
|
||||
@@ -148,7 +150,8 @@ object Utils {
|
||||
* get remote dns servers from preference
|
||||
*/
|
||||
fun getDomesticDnsServers(): List<String> {
|
||||
val domesticDns = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
|
||||
val domesticDns =
|
||||
MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
|
||||
val ret = domesticDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
|
||||
if (ret.isEmpty()) {
|
||||
return listOf(AppConfig.DNS_DIRECT)
|
||||
@@ -292,7 +295,7 @@ object Utils {
|
||||
|
||||
fun urlEncode(url: String): String {
|
||||
return try {
|
||||
URLEncoder.encode(url, Charsets.UTF_8.toString())
|
||||
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
url
|
||||
@@ -356,7 +359,11 @@ object Utils {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getUrlContentWithCustomUserAgent(urlStr: String?, timeout: Int = 30000, httpPort: Int = 0): String {
|
||||
fun getUrlContentWithCustomUserAgent(
|
||||
urlStr: String?,
|
||||
timeout: Int = 30000,
|
||||
httpPort: Int = 0
|
||||
): String {
|
||||
val url = URL(urlStr)
|
||||
val conn = if (httpPort == 0) {
|
||||
url.openConnection()
|
||||
@@ -389,7 +396,7 @@ object Utils {
|
||||
}
|
||||
|
||||
|
||||
fun setNightMode(context: Context) {
|
||||
fun setNightMode() {
|
||||
when (MmkvManager.decodeSettingsString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
|
||||
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
@@ -409,7 +416,8 @@ object Utils {
|
||||
}
|
||||
|
||||
fun getLocale(): Locale {
|
||||
val langCode = MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
|
||||
val langCode =
|
||||
MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
|
||||
val language = Language.fromCode(langCode)
|
||||
|
||||
return when (language) {
|
||||
@@ -421,6 +429,7 @@ object Utils {
|
||||
Language.RUSSIAN -> Locale("ru")
|
||||
Language.PERSIAN -> Locale("fa")
|
||||
Language.BANGLA -> Locale("bn")
|
||||
Language.BAKHTIARI -> Locale("bqi", "IR")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,7 +463,8 @@ object Utils {
|
||||
return if (second) {
|
||||
AppConfig.DelayTestUrl2
|
||||
} else {
|
||||
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DelayTestUrl
|
||||
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
|
||||
?: AppConfig.DelayTestUrl
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,5 +492,11 @@ object Utils {
|
||||
return false
|
||||
}
|
||||
|
||||
fun receiverFlags(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
} else {
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ object ZipUtil {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (filesToCompress.isEmpty) {
|
||||
if (filesToCompress.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.IntentFilter
|
||||
import android.content.res.AssetManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -25,6 +26,7 @@ import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.SpeedtestUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -44,20 +46,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val updateTestResultAction by lazy { MutableLiveData<String>() }
|
||||
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
|
||||
|
||||
/**
|
||||
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
||||
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||
*/
|
||||
|
||||
fun startListenBroadcast() {
|
||||
isRunning.value = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getApplication<AngApplication>().registerReceiver(
|
||||
mMsgReceiver,
|
||||
IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY),
|
||||
Context.RECEIVER_EXPORTED
|
||||
)
|
||||
} else {
|
||||
getApplication<AngApplication>().registerReceiver(
|
||||
mMsgReceiver,
|
||||
IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
||||
)
|
||||
}
|
||||
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
||||
ContextCompat.registerReceiver(getApplication(), mMsgReceiver, mFilter, Utils.receiverFlags())
|
||||
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "")
|
||||
}
|
||||
|
||||
@@ -260,10 +257,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
val deleteServer = mutableListOf<String>()
|
||||
serversCacheCopy.forEachIndexed { index, it ->
|
||||
val outbound = it.second.getKeyProperty()
|
||||
val outbound = it.second
|
||||
serversCacheCopy.forEachIndexed { index2, it2 ->
|
||||
if (index2 > index) {
|
||||
val outbound2 = it2.second.getKeyProperty()
|
||||
val outbound2 = it2.second
|
||||
if (outbound.equals(outbound2) && !deleteServer.contains(it2.first)) {
|
||||
deleteServer.add(it2.first)
|
||||
}
|
||||
@@ -81,7 +81,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
||||
// }
|
||||
}
|
||||
if (key == AppConfig.PREF_UI_MODE_NIGHT) {
|
||||
Utils.setNightMode(getApplication())
|
||||
Utils.setNightMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
object ShadowsocksFmt : FmtBase() {
|
||||
fun parse(str: String): ProfileItem? {
|
||||
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
||||
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
if (uri.idnHost.isEmpty() || uri.userInfo.isEmpty()) return null
|
||||
|
||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||
config.server = uri.idnHost
|
||||
config.serverPort = uri.port.toString()
|
||||
|
||||
val result = if (uri.userInfo.contains(":")) {
|
||||
uri.userInfo.split(":")
|
||||
} else {
|
||||
Utils.decode(uri.userInfo).split(":")
|
||||
}
|
||||
if (result.count() == 2) {
|
||||
config.method = result.first()
|
||||
config.password = result.last()
|
||||
}
|
||||
|
||||
if (!uri.rawQuery.isNullOrEmpty()) {
|
||||
val queryParam = getQueryParam(uri)
|
||||
|
||||
if (queryParam["plugin"] == "obfs-local" && queryParam["obfs"] == "http") {
|
||||
config.network = "tcp"
|
||||
config.headerType = "http"
|
||||
config.host = queryParam["obfs-host"]
|
||||
config.path = queryParam["path"]
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
fun toUri(config: ProfileItem): String {
|
||||
val pw = "${config.method}:${config.password}"
|
||||
|
||||
return toUri(config, pw, null)
|
||||
}
|
||||
|
||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
|
||||
|
||||
outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
server.address = profileItem.server.orEmpty()
|
||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||
server.password = profileItem.password
|
||||
server.method = profileItem.method
|
||||
}
|
||||
|
||||
return outboundBean
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user