diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f81ac8d4e4..2ce1f9075f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,13 +1,14 @@ name: Build APK on: - push: workflow_dispatch: inputs: - XRAY_CORE_VERSION: - description: 'Xray core version or commit hash' + release_tag: required: false - + type: string + push: + branches: + - master jobs: build: @@ -16,23 +17,108 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + submodules: 'recursive' + fetch-depth: '0' - - name: Setup Java - uses: actions/setup-java@v4 + - name: Restore cached libtun2socks + id: cache-libtun2socks-restore + uses: actions/cache/restore@v4 with: - distribution: 'temurin' - java-version: '17' + path: ${{ github.workspace }}/AndroidLibXrayLite/libs + key: libtun2socks-${{ runner.os }}-${{ hashFiles('.git/modules/badvpn/refs/heads/main') }}-${{ hashFiles('.git/modules/libancillary/refs/heads/shadowsocks-android') }} + + - name: Setup Android NDK + uses: nttld/setup-ndk@v1 + id: setup-ndk + # Same version as https://gitlab.com/fdroid/fdroiddata/metadata/com.v2ray.ang.yml + with: + ndk-version: r27 + add-to-path: true + link-to-sdk: true + local-cache: true + + - name: Restore Android Symlinks + run: | + directory="${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin" + find "$directory" -type l | while read link; do + current_target=$(readlink "$link") + new_target="$directory/$(basename "$current_target")" + ln -sf "$new_target" "$link" + echo "Changed $(basename "$link") from $current_target to $new_target" + done + + - name: Build libtun2socks + if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true' + run: | + bash compile-tun2socks.sh + tar -xvzf libtun2socks.so.tgz + env: + NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + + - name: Save libtun2socks + if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ github.workspace }}/libs + key: libtun2socks-${{ runner.os }}-${{ hashFiles('.git/modules/badvpn/refs/heads/main') }}-${{ hashFiles('.git/modules/libancillary/refs/heads/shadowsocks-android') }} + + - name: Copy libtun2socks + run: | + cp -r ${{ github.workspace }}/libs ${{ github.workspace }}/V2rayNG/app + + - name: Fetch AndroidLibXrayLite tag + run: | + pushd AndroidLibXrayLite + CURRENT_TAG=$(git describe --tags --abbrev=0) + echo "Current tag in this repo: $CURRENT_TAG" + echo "CURRENT_TAG=$CURRENT_TAG" >> $GITHUB_ENV + popd + + - name: Download libv2ray + uses: robinraju/release-downloader@v1 + with: + repository: '2dust/AndroidLibXrayLite' + tag: ${{ env.CURRENT_TAG }} + fileName: 'libv2ray.aar' + out-file-path: V2rayNG/app/libs/ + + - name: Restore cached libhysteria2 + id: cache-libhysteria2-restore + uses: actions/cache/restore@v4 + with: + path: ${{ github.workspace }}/hysteria/libs + key: libhysteria2-${{ runner.os }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }} - name: Setup Golang + if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true' uses: actions/setup-go@v5 with: - go-version: '1.22.2' + go-version-file: 'AndroidLibXrayLite/go.mod' - - name: Install gomobile + - name: Build libhysteria2 + if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true' run: | - go install golang.org/x/mobile/cmd/gomobile@latest - echo "$(go env GOPATH)/bin" >> $GITHUB_PATH + bash libhysteria2.sh + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + + - name: Save libhysteria2 + if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ github.workspace }}/hysteria/libs + key: libhysteria2-${{ runner.os }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }} + - name: Copy libhysteria2 + run: | + cp -r ${{ github.workspace }}/hysteria/libs ${{ github.workspace }}/V2rayNG/app + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' - name: Setup Android environment uses: android-actions/setup-android@v3 @@ -54,10 +140,37 @@ jobs: run: | cd ${{ github.workspace }}/V2rayNG chmod 755 gradlew - ./gradlew assembleDebug + ./gradlew licenseFdroidReleaseReport + ./gradlew assembleRelease -Pandroid.injected.signing.store.file=${{ steps.android_keystore.outputs.filePath }} -Pandroid.injected.signing.store.password=${{ secrets.APP_KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.APP_KEYSTORE_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.APP_KEY_PASSWORD }} + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - - name: Upload APK + - name: Upload arm64-v8a APK uses: actions/upload-artifact@v4 + if: ${{ success() }} + with: + name: arm64-v8a + path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*arm64-v8a*.apk + + - name: Upload armeabi-v7a APK + uses: actions/upload-artifact@v4 + if: ${{ success() }} + with: + name: armeabi-v7a + path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*armeabi-v7a*.apk + + - name: Upload x86 APK + uses: actions/upload-artifact@v4 + if: ${{ success() }} + with: + name: x86-apk + path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*x86*.apk + + - name: Upload to release + uses: svenstaro/upload-release-action@v2 + if: github.event.inputs.release_tag != '' with: - name: apk - path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/ + file: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*playstore*/release/*.apk + tag: ${{ github.event.inputs.release_tag }} + file_glob: true + prerelease: true diff --git a/.github/workflows/fastlane.yml b/.github/workflows/fastlane.yml new file mode 100644 index 0000000000..43a4632d2d --- /dev/null +++ b/.github/workflows/fastlane.yml @@ -0,0 +1,16 @@ +name: Validate Fastlane metadata + +on: + workflow_dispatch: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + go: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Validate Fastlane Supply Metadata + uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.0.0 diff --git a/.gitignore b/.gitignore index 0963c5091d..b844412dc8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ V2rayNG/app/release/output.json .idea/ .gradle/ +*.so diff --git a/AndroidLibV2rayLite/README.md b/AndroidLibV2rayLite/README.md deleted file mode 100644 index 118eefcf22..0000000000 --- a/AndroidLibV2rayLite/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# AndroidLibV2rayLite - -### Preparation -- latest Ubuntu environment -- At lease 30G free space -- Get Repo [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite) -### Prepare Go -- Go to https://golang.org/doc/install and install latest go -- Make sure `go version` works as expected -### Prepare gomobile -- Go to https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile and install gomobile -- export PATH=$PATH:~/go/bin -- Make sure `gomobile init` works as expected -### Prepare NDK -- Go to https://developer.android.com/ndk/downloads and install latest NDK -- export PATH=$PATH: -- Make sure `ndk-build -v` works as expected -### Make -- sudo apt install make -- Read and understand [build script](https://github.com/2dust/AndroidLibV2rayLite/blob/master/Makefile) diff --git a/V2rayNG/app/build.gradle.kts b/V2rayNG/app/build.gradle.kts index cde6dd8d66..3a006f9217 100644 --- a/V2rayNG/app/build.gradle.kts +++ b/V2rayNG/app/build.gradle.kts @@ -1,46 +1,63 @@ plugins { - id("com.android.application") - id("org.jetbrains.kotlin.android") + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + id("com.jaredsburrows.license") } android { namespace = "com.v2ray.ang" - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "com.MahsaNet.NikaNG" minSdk = 21 - targetSdk = 34 - versionCode = 583 - versionName = "1.8.38" + targetSdk = 35 + versionCode = 630 + versionName = "1.9.34" multiDexEnabled = true + + val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';') splits { abi { isEnable = true - include( - "arm64-v8a", - "armeabi-v7a", - "x86_64", - "x86" - ) - isUniversalApk = true + reset() + if (abiFilterList != null && abiFilterList.isNotEmpty()) { + include(*abiFilterList.toTypedArray()) + } else { + include( + "arm64-v8a", + "armeabi-v7a", + "x86_64", + "x86" + ) + } + isUniversalApk = abiFilterList.isNullOrEmpty() } } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } buildTypes { release { isMinifyEnabled = false - + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } - debug { - isMinifyEnabled = false + } + flavorDimensions.add("distribution") + productFlavors { + create("fdroid") { + dimension = "distribution" + applicationIdSuffix = ".fdroid" + buildConfigField("String", "DISTRIBUTION", "\"F-Droid\"") + } + create("playstore") { + dimension = "distribution" + buildConfigField("String", "DISTRIBUTION", "\"Play Store\"") } } @@ -50,30 +67,57 @@ android { } } + + compileOptions { + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() + jvmTarget = JavaVersion.VERSION_17.toString() } applicationVariants.all { val variant = this - val versionCodes = - mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4) - - variant.outputs - .map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl } - .forEach { output -> - val abi = if (output.getFilter("ABI") != null) - output.getFilter("ABI") - else - "universal" - - output.outputFileName = "NikaNG_${variant.versionName}_${abi}.apk" - if (versionCodes.containsKey(abi)) { - output.versionCodeOverride = (1000000 * versionCodes[abi]!!).plus(variant.versionCode) - } else { - return@forEach + val isFdroid = variant.productFlavors.any { it.name == "fdroid" } + if (isFdroid) { + val versionCodes = + mapOf("armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0 + ) + + variant.outputs + .map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl } + .forEach { output -> + val abi = output.getFilter("ABI") ?: "universal" + output.outputFileName = "NikaNG_${variant.versionName}-fdroid_${abi}.apk" + if (versionCodes.containsKey(abi)) { + output.versionCodeOverride = + (100 * variant.versionCode + versionCodes[abi]!!).plus(5000000) + } else { + return@forEach + } } - } + } else { + val versionCodes = + mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4) + + variant.outputs + .map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl } + .forEach { output -> + val abi = if (output.getFilter("ABI") != null) + output.getFilter("ABI") + else + "universal" + + output.outputFileName = "NikaNG_${variant.versionName}_${abi}.apk" + if (versionCodes.containsKey(abi)) { + output.versionCodeOverride = + (1000000 * versionCodes[abi]!!).plus(variant.versionCode) + } else { + return@forEach + } + } + } } buildFeatures { @@ -86,47 +130,62 @@ android { useLegacyPackaging = true } } + } dependencies { + // Core Libraries implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar")))) - testImplementation(libs.junit) - 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) - implementation(libs.lifecycle.viewmodel.ktx) - implementation(libs.lifecycle.livedata.ktx) - implementation(libs.lifecycle.runtime.ktx) + implementation(libs.androidx.swiperefreshlayout) - //kotlin - implementation(libs.kotlin.reflect) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.coroutines.android) + // 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) - implementation(libs.toastcompat) - implementation(libs.editorkit) + + // Language and Processing Libraries implementation(libs.language.base) implementation(libs.language.json) - implementation(libs.quickie.bundled) + + // Intent and Utility Libraries + implementation(libs.quickie.foss) implementation(libs.core) + + // AndroidX Lifecycle and Architecture Components + implementation(libs.lifecycle.viewmodel.ktx) + implementation(libs.lifecycle.livedata.ktx) + implementation(libs.lifecycle.runtime.ktx) + + // 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) + coreLibraryDesugaring(libs.desugar.jdk.libs) } \ No newline at end of file diff --git a/V2rayNG/app/libs/json.jar b/V2rayNG/app/libs/json.jar new file mode 100644 index 0000000000..abdb16b764 Binary files /dev/null and b/V2rayNG/app/libs/json.jar differ diff --git a/V2rayNG/app/proguard-rules.pro b/V2rayNG/app/proguard-rules.pro index e69de29bb2..481bb43481 100644 --- a/V2rayNG/app/proguard-rules.pro +++ b/V2rayNG/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml index d5301680ab..c2cde774c9 100644 --- a/V2rayNG/app/src/main/AndroidManifest.xml +++ b/V2rayNG/app/src/main/AndroidManifest.xml @@ -5,9 +5,9 @@ --> - + + android:name=".ui.SettingsActivity" + android:exported="false" /> + android:name=".ui.PerAppProxyActivity" + android:exported="false" /> + android:name=".ui.ScannerActivity" + android:exported="false" /> + android:name=".ui.LogcatActivity" + android:exported="false" /> + android:name=".ui.RoutingSettingActivity" + android:exported="false" /> + android:name=".ui.RoutingEditActivity" + android:exported="false" /> + android:name=".ui.SubSettingActivity" + android:exported="false" /> + android:name=".ui.UserAssetActivity" + android:exported="false" /> + + android:name=".ui.SubEditActivity" + android:exported="false" /> + android:name=".ui.ScScannerActivity" + android:exported="false" /> + android:name=".ui.UrlSchemeActivity" + android:exported="true"> @@ -143,16 +145,16 @@ + android:name=".ui.AboutActivity" + android:exported="false" /> @@ -168,8 +170,8 @@ + android:process=":RunSoLibV2RayDaemon" /> + + + + + @@ -212,19 +222,19 @@ + android:exported="true" + android:icon="@mipmap/ic_launcher"> + android:exported="true" + android:process=":RunSoLibV2RayDaemon" + tools:ignore="ExportedReceiver"> diff --git a/V2rayNG/app/src/main/assets/custom_routing_black b/V2rayNG/app/src/main/assets/custom_routing_black new file mode 100644 index 0000000000..ce0c74403b --- /dev/null +++ b/V2rayNG/app/src/main/assets/custom_routing_black @@ -0,0 +1,149 @@ +[ + { + "remarks": "绕过bittorrent", + "outboundTag": "direct", + "protocol": [ + "bittorrent" + ] + }, + { + "remarks": "Google cn", + "outboundTag": "proxy", + "domain": [ + "domain:googleapis.cn", + "domain:gstatic.com" + ] + }, + { + "remarks": "阻断udp443", + "outboundTag": "block", + "port": "443", + "network": "udp" + }, + { + "remarks": "阻断广告", + "outboundTag": "block", + "domain": [ + "geosite:category-ads-all" + ] + }, + { + "remarks": "绕过局域网IP", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + }, + { + "remarks": "绕过局域网域名", + "outboundTag": "direct", + "domain": [ + "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": [ + "geoip:facebook", + "geoip:fastly", + "geoip:google", + "geoip:netflix", + "geoip:telegram", + "geoip:twitter" + ] + }, + { + "remarks": "代理GFW", + "outboundTag": "proxy", + "domain": [ + "geosite:gfw", + "geosite:greatfire" + ] + }, + { + "remarks": "最终直连", + "port": "0-65535", + "outboundTag": "direct" + } +] \ No newline at end of file diff --git a/V2rayNG/app/src/main/assets/custom_routing_block b/V2rayNG/app/src/main/assets/custom_routing_block deleted file mode 100644 index a6ebf39410..0000000000 --- a/V2rayNG/app/src/main/assets/custom_routing_block +++ /dev/null @@ -1 +0,0 @@ -geosite:category-ads-all, \ No newline at end of file diff --git a/V2rayNG/app/src/main/assets/custom_routing_direct b/V2rayNG/app/src/main/assets/custom_routing_direct deleted file mode 100644 index 083428bfce..0000000000 --- a/V2rayNG/app/src/main/assets/custom_routing_direct +++ /dev/null @@ -1 +0,0 @@ -geosite:cn \ No newline at end of file diff --git a/V2rayNG/app/src/main/assets/custom_routing_global b/V2rayNG/app/src/main/assets/custom_routing_global new file mode 100644 index 0000000000..99006d9bd8 --- /dev/null +++ b/V2rayNG/app/src/main/assets/custom_routing_global @@ -0,0 +1,34 @@ +[ + { + "remarks": "阻断udp443", + "outboundTag": "block", + "port": "443", + "network": "udp" + }, + { + "remarks": "阻断广告", + "outboundTag": "block", + "domain": [ + "geosite:category-ads-all" + ] + }, + { + "remarks": "绕过局域网IP", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + }, + { + "remarks": "绕过局域网域名", + "outboundTag": "direct", + "domain": [ + "geosite:private" + ] + }, + { + "remarks": "最终代理", + "port": "0-65535", + "outboundTag": "proxy" + } +] \ No newline at end of file diff --git a/V2rayNG/app/src/main/assets/custom_routing_iran_heavy b/V2rayNG/app/src/main/assets/custom_routing_iran_heavy new file mode 100644 index 0000000000..764b8be8e9 --- /dev/null +++ b/V2rayNG/app/src/main/assets/custom_routing_iran_heavy @@ -0,0 +1,72 @@ +[ + { + "remarks": "Block ADS using c4u", + "outboundTag": "block", + "domain": [ + "ext:geosite_c4u.dat:category-ads-all" + ] + }, + { + "remarks": "Block Malicious using c4u", + "outboundTag": "block", + "domain": [ + "ext:geosite_c4u.dat:nsfw", + "ext:geosite_c4u.dat:malware", + "ext:geosite_c4u.dat:phishing", + "ext:geosite_c4u.dat:cryptominers" + ] + }, + { + "remarks": "Block malware IP using c4u", + "outboundTag": "block", + "ip": [ + "ext:geoip_c4u.dat:malware", + "ext:geoip_c4u.dat:phishing" + ] + }, + { + "remarks": "Bypass Iran domains using c4u", + "outboundTag": "direct", + "domain": [ + "ext:geosite_c4u.dat:ir" + ] + }, + { + "remarks": "Bypass .ir Domain by Regex", + "outboundTag": "direct", + "domain": [ + "regexp:.*\\.ir$", + "regexp:.*\\.xn--mgba3a4f16a$" + ] + }, + { + "remarks": "Bypass Iran IP using c4u", + "outboundTag": "direct", + "ip": [ + "ext:geoip_c4u.dat:ir" + ] + }, + { + "remarks": "Block 10.10.34.34", + "outboundTag": "block", + "ip": [ + "10.10.34.34", + "10.10.34.35", + "10.10.34.36" + ] + }, + { + "remarks": "Direct LAN domains", + "outboundTag": "direct", + "domain": [ + "geosite:private" + ] + }, + { + "remarks": "Direct LAN IP", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + } +] diff --git a/V2rayNG/app/src/main/assets/custom_routing_iran_lite b/V2rayNG/app/src/main/assets/custom_routing_iran_lite new file mode 100644 index 0000000000..f90cbdb876 --- /dev/null +++ b/V2rayNG/app/src/main/assets/custom_routing_iran_lite @@ -0,0 +1,47 @@ +[ + { + "remarks": "Block ads and trackers", + "outboundTag": "block", + "domain": [ + "geosite:category-ads-all" + ] + }, + { + "remarks": "Bypass Iran domains", + "outboundTag": "direct", + "domain": [ + "domain:ir", + "geosite:category-ir" + ] + }, + { + "remarks": "Bypass Iran IP", + "outboundTag": "direct", + "ip": [ + "geoip:ir" + ] + }, + { + "remarks": "Block 10.10.34.34", + "outboundTag": "block", + "ip": [ + "10.10.34.34", + "10.10.34.35", + "10.10.34.36" + ] + }, + { + "remarks": "Direct LAN domains", + "outboundTag": "direct", + "domain": [ + "geosite:private" + ] + }, + { + "remarks": "Direct LAN IP", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + } +] diff --git a/V2rayNG/app/src/main/assets/custom_routing_iran_medium b/V2rayNG/app/src/main/assets/custom_routing_iran_medium new file mode 100644 index 0000000000..ce0f074e57 --- /dev/null +++ b/V2rayNG/app/src/main/assets/custom_routing_iran_medium @@ -0,0 +1,54 @@ +[ + { + "remarks": "Block ADS using c4u", + "outboundTag": "block", + "domain": [ + "ext:geosite_c4u.dat:category-ads-all" + ] + }, + { + "remarks": "Block porn", + "outboundTag": "block", + "domain": [ + "geosite:category-porn" + ] + }, + { + "remarks": "Bypass Iran domains", + "outboundTag": "direct", + "domain": [ + "domain:ir", + "geosite:category-ir" + ] + }, + { + "remarks": "Bypass Iran IP", + "outboundTag": "direct", + "ip": [ + "geoip:ir" + ] + }, + { + "remarks": "Block 10.10.34.34", + "outboundTag": "block", + "ip": [ + "10.10.34.34", + "10.10.34.35", + "10.10.34.36" + ] + }, + { + "remarks": "Direct LAN domains", + "outboundTag": "direct", + "domain": [ + "geosite:private" + ] + }, + { + "remarks": "Direct LAN IP", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + } +] diff --git a/V2rayNG/app/src/main/assets/custom_routing_proxy b/V2rayNG/app/src/main/assets/custom_routing_proxy deleted file mode 100644 index 618b14017a..0000000000 --- a/V2rayNG/app/src/main/assets/custom_routing_proxy +++ /dev/null @@ -1 +0,0 @@ -geosite:geolocation-!cn \ No newline at end of file diff --git a/V2rayNG/app/src/main/assets/custom_routing_white b/V2rayNG/app/src/main/assets/custom_routing_white new file mode 100644 index 0000000000..214538480e --- /dev/null +++ b/V2rayNG/app/src/main/assets/custom_routing_white @@ -0,0 +1,103 @@ +[ + { + "remarks": "Google cn", + "outboundTag": "proxy", + "domain": [ + "domain:googleapis.cn", + "domain:gstatic.com" + ] + }, + { + "remarks": "阻断udp443", + "outboundTag": "block", + "port": "443", + "network": "udp" + }, + { + "remarks": "阻断广告", + "outboundTag": "block", + "domain": [ + "geosite:category-ads-all" + ] + }, + { + "remarks": "绕过局域网IP", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + }, + { + "remarks": "绕过局域网域名", + "outboundTag": "direct", + "domain": [ + "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": [ + "geoip:cn" + ] + }, + { + "remarks": "绕过中国域名", + "outboundTag": "direct", + "domain": [ + "geosite:cn" + ] + } +] \ No newline at end of file diff --git a/V2rayNG/app/src/main/assets/custom_routing_white_iran b/V2rayNG/app/src/main/assets/custom_routing_white_iran new file mode 100644 index 0000000000..641a556941 --- /dev/null +++ b/V2rayNG/app/src/main/assets/custom_routing_white_iran @@ -0,0 +1,44 @@ +[ + { + "remarks": "Block udp443", + "outboundTag": "block", + "port": "443", + "network": "udp" + }, + { + "remarks": "Block ads and trackers", + "outboundTag": "block", + "domain": [ + "geosite:category-ads-all" + ] + }, + { + "remarks": "Direct LAN IP", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + }, + { + "remarks": "Direct LAN domains", + "outboundTag": "direct", + "domain": [ + "geosite:private" + ] + }, + { + "remarks": "Bypass Iran domains", + "outboundTag": "direct", + "domain": [ + "domain:ir", + "geosite:category-ir" + ] + }, + { + "remarks": "Bypass Iran IP", + "outboundTag": "direct", + "ip": [ + "geoip:ir" + ] + } +] diff --git a/V2rayNG/app/src/main/assets/open_source_licenses.html b/V2rayNG/app/src/main/assets/open_source_licenses.html new file mode 100644 index 0000000000..0cfd1daa68 --- /dev/null +++ b/V2rayNG/app/src/main/assets/open_source_licenses.html @@ -0,0 +1,1285 @@ + + + + + Open source licenses + + +

Notice for packages:

+
    +
  • Camera Core +
    +
    Copyright © 2019 The Android Open Source Project
    +
    +
    +
  • +
+ +
                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
+
+
+
BSD License
+https://chromium.googlesource.com/libyuv/libyuv/+/refs/heads/main/README.chromium
+
+
+ + +
                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
+
+
+
+
    +
  • MMKV +
    +
    Copyright © 20xx Tencent Wechat, Inc.
    +
    +
    +
  • +
+ +
BSD 3-Clause License
+
+Copyright (c) [year], [fullname]
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+ + +
Android Software Development Kit License
+https://developer.android.com/studio/terms.html
+
+
+ + +
ML Kit Terms of Service
+https://developers.google.com/ml-kit/terms
+
+
+
    +
  • editorkit +
    +
    Copyright © 20xx Dmitrii Rubtsov
    +
    +
    +
  • +
  • language-base +
    +
    Copyright © 20xx Dmitrii Rubtsov
    +
    +
    +
  • +
  • language-json +
    +
    Copyright © 20xx Dmitrii Rubtsov
    +
    +
    +
  • +
+ +
Apache 2.0 License
+https://github.com/massivemadness/EditorKit/blob/master/LICENSE
+
+
+ + +
MIT-0
+https://spdx.org/licenses/MIT-0.html
+
+
+ + +
MIT License
+
+Copyright (c) [year] [fullname]
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+ + diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt similarity index 81% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt index a07e7f9943..6b437021d5 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt @@ -1,10 +1,14 @@ package com.v2ray.ang import android.content.Context +import android.content.pm.PackageManager +import android.os.Build 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 class AngApplication : MultiDexApplication() { @@ -19,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() { @@ -32,11 +36,13 @@ class AngApplication : MultiDexApplication() { // if (firstRun) // defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply() - //Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE) MMKV.initialize(this) - Utils.setNightMode(application) + Utils.setNightMode() // Initialize WorkManager with the custom configuration WorkManager.initialize(this, workManagerConfiguration) + + SettingsManager.initRoutingRulesets(this) } + } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt similarity index 73% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt index a67ebbff79..ae5f5f97b8 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt @@ -22,14 +22,12 @@ object AppConfig { const val PREF_BYPASS_APPS = "pref_bypass_apps" const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled" const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled" + const val PREF_APPEND_HTTP_PROXY = "pref_append_http_proxy" const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port" const val PREF_VPN_DNS = "pref_vpn_dns" + const val PREF_VPN_BYPASS_LAN = "pref_vpn_bypass_lan" const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy" - const val PREF_ROUTING_MODE = "pref_routing_mode" - const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent" - const val PREF_V2RAY_ROUTING_DIRECT = "pref_v2ray_routing_direct" - const val PREF_V2RAY_ROUTING_BLOCKED = "pref_v2ray_routing_blocked" - const val PREF_ROUTING_CUSTOM = "pref_routing_custom" + const val PREF_ROUTING_RULESET = "pref_routing_ruleset" const val PREF_MUX_ENABLED = "pref_mux_enabled" const val PREF_MUX_CONCURRENCY = "pref_mux_concurrency" const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurrency" @@ -51,21 +49,19 @@ object AppConfig { const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled" const val PREF_ALLOW_INSECURE = "pref_allow_insecure" const val PREF_SOCKS_PORT = "pref_socks_port" - const val PREF_HTTP_PORT = "pref_http_port" - const val PREF_REMOTE_DNS = "pref_remote_dns" const val PREF_DOMESTIC_DNS = "pref_domestic_dns" + const val PREF_DNS_HOSTS = "pref_dns_hosts" const val PREF_DELAY_TEST_URL = "pref_delay_test_url" const val PREF_LOGLEVEL = "pref_core_loglevel" const val PREF_MODE = "pref_mode" + const val PREF_IS_BOOTED = "pref_is_booted" /** Cache keys. */ const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id" const val CACHE_KEYWORD_FILTER = "cache_keyword_filter" /** Protocol identifiers. */ - const val PROTOCOL_HTTP: String = "http://" - const val PROTOCOL_HTTPS: String = "https://" const val PROTOCOL_FREEDOM: String = "freedom" /** Broadcast actions. */ @@ -100,7 +96,8 @@ object AppConfig { const val v2rayNGWikiMode = "$v2rayNGUrl/wiki/Mode" const val v2rayNGPrivacyPolicy = "https://raw.githubusercontent.com/2dust/v2rayNG/master/CR.md" const val PromotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw=" - const val GeoUrl = "https://github.com/Chocolate4U/Iran-v2ray-rules/releases/latest/download/" + const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/" + const val GeoUrl_c4u = "https://github.com/Chocolate4U/Iran-v2ray-rules/releases/latest/download/" const val TgChannelUrl = "https://t.me/github_2dust" const val DelayTestUrl = "https://www.gstatic.com/generate_204" const val DelayTestUrl2 = "https://www.google.com/generate_204" @@ -109,14 +106,17 @@ object AppConfig { const val DNS_PROXY = "1.1.1.1" const val DNS_DIRECT = "1.1.1.1" const val DNS_VPN = "1.1.1.1" + const val GEOSITE_PRIVATE = "geosite:private" + const val GEOSITE_CN = "geosite:cn" + const val GEOIP_PRIVATE = "geoip:private" + const val GEOIP_CN = "geoip:cn" /** Ports and addresses for various services. */ const val PORT_LOCAL_DNS = "10853" const val PORT_SOCKS = "10808" - const val PORT_HTTP = "10809" const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32" const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128" - const val WIREGUARD_LOCAL_MTU = "1420" + const val WIREGUARD_LOCAL_MTU = "1280" const val WIREGUARD_keep_alive = "5" @@ -125,6 +125,8 @@ object AppConfig { const val WIREGUARD_wnoisedelay = "1-2" const val WIREGUARD_wpayloadsize = "5-10" + const val LOOPBACK = "127.0.0.1" + /** Message constants for communication. */ const val MSG_REGISTER_CLIENT = 1 @@ -145,7 +147,7 @@ object AppConfig { /** Notification channel IDs and names. */ const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID" - const val RAY_NG_CHANNEL_NAME = "V2rayNG Background Service" + const val RAY_NG_CHANNEL_NAME = "v2rayNG Background Service" const val SUBSCRIPTION_UPDATE_CHANNEL = "subscription_update_channel" const val SUBSCRIPTION_UPDATE_CHANNEL_NAME = "Subscription Update Service" @@ -154,7 +156,43 @@ object AppConfig { const val CUSTOM = "" const val SHADOWSOCKS = "ss://" const val SOCKS = "socks://" + const val HTTP = "http://" const val VLESS = "vless://" const val TROJAN = "trojan://" const val WIREGUARD = "wireguard://" + const val TUIC = "tuic://" + const val HYSTERIA2 = "hysteria2://" + const val HY2 = "hy2://" + + /** Give a good name to this, IDK*/ + const val VPN = "VPN" + + // Google API rule constants + const val GOOGLEAPIS_CN_DOMAIN = "domain:googleapis.cn" + const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com" + + // Android Private DNS constants + const val DNS_DNSPOD_DOMAIN = "dot.pub" + const val DNS_ALIDNS_DOMAIN = "dns.alidns.com" + 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_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1") + 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" + const val DEFAULT_LEVEL = 8 + const val DEFAULT_NETWORK = "tcp" + const val TLS = "tls" + const val REALITY = "reality" + const val HEADER_TYPE_HTTP = "http" + } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt similarity index 100% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AssetUrlItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt similarity index 100% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AssetUrlItem.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt new file mode 100644 index 0000000000..c0b70c6ad6 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt @@ -0,0 +1,9 @@ +package com.v2ray.ang.dto + +data class ConfigResult( + var status: Boolean, + var guid: String? = null, + var content: String = "", + var domainPort: String? = null, +) + diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/EConfigType.kt similarity index 62% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/dto/EConfigType.kt index 0cde22f0e2..fd1578e581 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/EConfigType.kt @@ -10,9 +10,12 @@ enum class EConfigType(val value: Int, val protocolScheme: String) { SOCKS(4, AppConfig.SOCKS), VLESS(5, AppConfig.VLESS), TROJAN(6, AppConfig.TROJAN), - WIREGUARD(7, AppConfig.WIREGUARD); + WIREGUARD(7, AppConfig.WIREGUARD), +// TUIC(8, AppConfig.TUIC), + HYSTERIA2(9, AppConfig.HYSTERIA2), + HTTP(10, AppConfig.HTTP); companion object { - fun fromInt(value: Int) = values().firstOrNull { it.value == value } + fun fromInt(value: Int) = entries.firstOrNull { it.value == value } } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Hysteria2Bean.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Hysteria2Bean.kt new file mode 100644 index 0000000000..c4fc658214 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Hysteria2Bean.kt @@ -0,0 +1,46 @@ +package com.v2ray.ang.dto + +data class Hysteria2Bean( + val server: String?, + val auth: String?, + val lazy: Boolean? = true, + val obfs: ObfsBean? = null, + val socks5: Socks5Bean? = null, + val http: Socks5Bean? = null, + val tls: TlsBean? = null, + val transport: TransportBean? = null, + val bandwidth: BandwidthBean? = null, +) { + data class ObfsBean( + val type: String?, + val salamander: SalamanderBean? + ) { + data class SalamanderBean( + val password: String?, + ) + } + + data class Socks5Bean( + val listen: String?, + ) + + data class TlsBean( + val sni: String?, + val insecure: Boolean?, + val pinSHA256: String?, + ) + + data class TransportBean( + val type: String?, + val udp: TransportUdpBean? + ) { + data class TransportUdpBean( + val hopInterval: String?, + ) + } + + data class BandwidthBean( + val down: String?, + val up: String?, + ) +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Language.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Language.kt new file mode 100644 index 0000000000..c3a8d9ba4e --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Language.kt @@ -0,0 +1,19 @@ +package com.v2ray.ang.dto + +enum class Language(val code: String) { + AUTO("auto"), + ENGLISH("en"), + CHINA("zh-rCN"), + TRADITIONAL_CHINESE("zh-rTW"), + VIETNAMESE("vi"), + RUSSIAN("ru"), + PERSIAN("fa"), + BANGLA("bn"), + BAKHTIARI("bqi-rIR"); + + companion object { + fun fromCode(code: String): Language { + return entries.find { it.code == code } ?: AUTO + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt new file mode 100644 index 0000000000..ba285665f9 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt @@ -0,0 +1,17 @@ +package com.v2ray.ang.dto + +enum class NetworkType(val type: String) { + TCP("tcp"), + KCP("kcp"), + WS("ws"), + HTTP_UPGRADE("httpupgrade"), + XHTTP("xhttp"), + HTTP("http"), + H2("h2"), + QUIC("quic"), + GRPC("grpc"); + + companion object { + fun fromString(type: String?) = entries.find { it.type == type } ?: TCP + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileItem.kt new file mode 100644 index 0000000000..52cc57f46e --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileItem.kt @@ -0,0 +1,133 @@ +package com.v2ray.ang.dto + +import com.v2ray.ang.AppConfig.LOOPBACK +import com.v2ray.ang.AppConfig.PORT_SOCKS +import com.v2ray.ang.AppConfig.TAG_BLOCKED +import com.v2ray.ang.AppConfig.TAG_DIRECT +import com.v2ray.ang.AppConfig.TAG_PROXY +import com.v2ray.ang.util.Utils + +data class ProfileItem( + val configVersion: Int = 4, + val configType: EConfigType, + var subscriptionId: String = "", + var addedTime: Long = System.currentTimeMillis(), + + var remarks: String = "", + var server: String? = null, + var serverPort: String? = null, + + var password: String? = null, + var method: String? = null, + var flow: String? = null, + var username: String? = null, + + var network: String? = null, + var headerType: String? = null, + var host: String? = null, + var path: String? = null, + var seed: String? = null, + var quicSecurity: String? = null, + var quicKey: String? = null, + 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, + var alpn: String? = null, + var fingerPrint: String? = null, + var insecure: Boolean? = null, + + var publicKey: String? = null, + var shortId: String? = null, + var spiderX: String? = null, + + var secretKey: String? = null, + var preSharedKey: String? = null, + var localAddress: String? = null, + var reserved: String? = null, + var mtu: Int? = null, + var keepAlive: Int? = null, + var wnoise: String? = null, + var wnoisecount: String? = null, + var wnoisedelay: String? = null, + var wpayloadsize: String? = null, + + + + var obfsPassword: String? = null, + var portHopping: String? = null, + var portHoppingInterval: String? = null, + var pinSHA256: String? = null, + var bandwidthDown: String? = null, + var bandwidthUp: String? = null, + + ) { + companion object { + fun create(configType: EConfigType): ProfileItem { + return ProfileItem(configType = configType) + } + } + + fun getAllOutboundTags(): MutableList { + return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED) + } + + fun getServerAddressAndPort(): String { + if (server.isNullOrEmpty() && configType == EConfigType.CUSTOM) { + return "$LOOPBACK:$PORT_SOCKS" + } + return Utils.getIpv6Address(server) + ":" + serverPort + } + + 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.keepAlive == obj.keepAlive + && this.wnoise == obj.wnoise + && this.wnoisecount == obj.wnoisecount + && this.wnoisedelay == obj.wnoisedelay + && this.wpayloadsize == obj.wpayloadsize + + && this.obfsPassword == obj.obfsPassword + && this.portHopping == obj.portHopping + && this.portHoppingInterval == obj.portHoppingInterval + && this.pinSHA256 == obj.pinSHA256 + ) + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ProfileItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileLiteItem.kt similarity index 86% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ProfileItem.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileLiteItem.kt index a906289cdd..12995abd76 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ProfileItem.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileLiteItem.kt @@ -1,6 +1,6 @@ package com.v2ray.ang.dto -data class ProfileItem( +data class ProfileLiteItem( val configType: EConfigType, var subscriptionId: String = "", var remarks: String = "", diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RoutingType.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RoutingType.kt new file mode 100644 index 0000000000..7c93718e08 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RoutingType.kt @@ -0,0 +1,24 @@ +package com.v2ray.ang.dto + +enum class RoutingType(val fileName: String) { + WHITE("custom_routing_white"), + BLACK("custom_routing_black"), + GLOBAL("custom_routing_global"), + IRAN_LITE("custom_routing_iran_lite"), + IRAN_MEDIUM("custom_routing_iran_medium"), + IRAN_HEAVY("custom_routing_iran_heavy"); + + companion object { + fun fromIndex(index: Int): RoutingType { + return when (index) { + 0 -> WHITE + 1 -> BLACK + 2 -> GLOBAL + 3 -> IRAN_LITE + 4 -> IRAN_MEDIUM + 5 -> IRAN_HEAVY + else -> WHITE + } + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RulesetItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RulesetItem.kt new file mode 100644 index 0000000000..9005f559dc --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RulesetItem.kt @@ -0,0 +1,13 @@ +package com.v2ray.ang.dto + +data class RulesetItem( + var remarks: String? = "", + var ip: List? = null, + var domain: List? = null, + var outboundTag: String = "", + var port: String? = null, + var network: String? = null, + var protocol: List? = null, + var enabled: Boolean = true, + var locked: Boolean? = false, +) \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerAffiliationInfo.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerAffiliationInfo.kt similarity index 100% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerAffiliationInfo.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerAffiliationInfo.kt diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerConfig.kt similarity index 88% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerConfig.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerConfig.kt index f2a91e2e8c..baa1fe48f4 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerConfig.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerConfig.kt @@ -3,7 +3,6 @@ package com.v2ray.ang.dto import com.v2ray.ang.AppConfig.TAG_BLOCKED import com.v2ray.ang.AppConfig.TAG_DIRECT import com.v2ray.ang.AppConfig.TAG_PROXY -import com.v2ray.ang.util.Utils data class ServerConfig( val configVersion: Int = 3, @@ -17,7 +16,8 @@ data class ServerConfig( companion object { fun create(configType: EConfigType): ServerConfig { when (configType) { - EConfigType.VMESS, EConfigType.VLESS -> + EConfigType.VMESS, + EConfigType.VLESS -> return ServerConfig( configType = configType, outboundBean = V2rayConfig.OutboundBean( @@ -36,7 +36,11 @@ data class ServerConfig( EConfigType.CUSTOM -> return ServerConfig(configType = configType) - EConfigType.SHADOWSOCKS, EConfigType.SOCKS, EConfigType.TROJAN -> + EConfigType.SHADOWSOCKS, + EConfigType.SOCKS, + EConfigType.HTTP, + EConfigType.TROJAN, + EConfigType.HYSTERIA2 -> return ServerConfig( configType = configType, outboundBean = V2rayConfig.OutboundBean( @@ -79,10 +83,4 @@ data class ServerConfig( } return mutableListOf() } - - fun getV2rayPointDomainAndPort(): String { - val address = getProxyOutbound()?.getServerAddress().orEmpty() - val port = getProxyOutbound()?.getServerPort() - return Utils.getIpv6Address(address) + ":" + port - } } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServersCache.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServersCache.kt similarity index 100% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServersCache.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServersCache.kt diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt similarity index 74% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt index 056dff6e0a..8e8c66a403 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt @@ -8,5 +8,8 @@ data class SubscriptionItem( var lastUpdated: Long = -1, var autoUpdate: Boolean = false, val updateInterval: Int? = null, + var prevProfile: String? = null, + var nextProfile: String? = null, + var filter: String? = null, ) diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt similarity index 64% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt index 3223fc714d..45d7a25155 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt @@ -7,6 +7,10 @@ import com.google.gson.JsonSerializationContext import com.google.gson.JsonSerializer import com.google.gson.annotations.SerializedName import com.google.gson.reflect.TypeToken +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.* +import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.* +import com.v2ray.ang.util.Utils import java.lang.reflect.Type data class V2rayConfig( @@ -26,16 +30,6 @@ data class V2rayConfig( var observatory: Any? = null, var burstObservatory: Any? = null ) { - companion object { - const val DEFAULT_PORT = 443 - const val DEFAULT_SECURITY = "auto" - const val DEFAULT_LEVEL = 8 - const val DEFAULT_NETWORK = "tcp" - - const val TLS = "tls" - const val REALITY = "reality" - const val HTTP = "http" - } data class LogBean( val access: String, @@ -81,46 +75,92 @@ data class V2rayConfig( val sendThrough: String? = null, var mux: MuxBean? = MuxBean(false) ) { + companion object { + fun create(configType: EConfigType): OutboundBean? { + return when (configType) { + EConfigType.VMESS, + EConfigType.VLESS -> + return OutboundBean( + protocol = configType.name.lowercase(), + settings = OutSettingsBean( + vnext = listOf( + VnextBean( + users = listOf(UsersBean()) + ) + ) + ), + streamSettings = StreamSettingsBean() + ) - data class OutSettingsBean(var vnext: List? = null, - var fragment: FragmentBean? = null, - var servers: List? = null, - /*Blackhole*/ - var response: Response? = null, - /*DNS*/ - val network: String? = null, - var address: Any? = null, - val port: Int? = null, - /*Freedom*/ - var domainStrategy: String? = null, - val redirect: String? = null, - val userLevel: Int? = null, - /*Loopback*/ - val inboundTag: String? = null, - /*Wireguard*/ - var secretKey: String? = null, - val peers: List? = null, - var reserved: List? = null, - var mtu :Int? = null, - var wnoise: String? = null, - var wnoisecount: String? = null, - var wnoisedelay: String? = null, - var wpayloadsize: String? = null - ) { + EConfigType.SHADOWSOCKS, + EConfigType.SOCKS, + EConfigType.HTTP, + EConfigType.TROJAN, + EConfigType.HYSTERIA2 -> + return OutboundBean( + protocol = configType.name.lowercase(), + settings = OutSettingsBean( + servers = listOf(ServersBean()) + ), + streamSettings = StreamSettingsBean() + ) + EConfigType.WIREGUARD -> + return OutboundBean( + protocol = configType.name.lowercase(), + settings = OutSettingsBean( + secretKey = "", + peers = listOf(WireGuardBean()) + ) + ) + EConfigType.CUSTOM -> null + } + } + } - data class VnextBean(var address: String = "", - var port: Int = DEFAULT_PORT, - var users: List) { + data class OutSettingsBean( + var vnext: List? = null, + var fragment: FragmentBean? = null, + var noises: List? = null, + var servers: List? = null, + /*Blackhole*/ + var response: Response? = null, + /*DNS*/ + val network: String? = null, + var address: Any? = null, + val port: Int? = null, + /*Freedom*/ + var domainStrategy: String? = null, + val redirect: String? = null, + val userLevel: Int? = null, + /*Loopback*/ + val inboundTag: String? = null, + /*Wireguard*/ + var secretKey: String? = null, + val peers: List? = null, + var reserved: List? = null, + var mtu: Int? = null, + var obfsPassword: String? = null, + var wnoise: String? = null, + var wnoisecount: String? = null, + var wnoisedelay: String? = null, + var wpayloadsize: String? = null, + ) { + + data class VnextBean( + var address: String = "", + var port: Int = AppConfig.DEFAULT_PORT, + var users: List + ) { data class UsersBean( var id: String = "", var alterId: Int? = null, - var security: String = DEFAULT_SECURITY, - var level: Int = DEFAULT_LEVEL, - var encryption: String = "", - var flow: String = "" + var security: String? = null, + var level: Int = AppConfig.DEFAULT_LEVEL, + var encryption: String? = null, + var flow: String? = null ) } @@ -130,47 +170,55 @@ data class V2rayConfig( var interval: String? = null ) + data class NoiseBean( + var type: String? = null, + var packet: String? = null, + var delay: String? = null + ) + data class ServersBean( var address: String = "", - var method: String = "chacha20-poly1305", + var method: String? = null, var ota: Boolean = false, - var password: String = "", - var port: Int = DEFAULT_PORT, - var level: Int = DEFAULT_LEVEL, + var password: String? = null, + var port: Int = AppConfig.DEFAULT_PORT, + var level: Int = AppConfig.DEFAULT_LEVEL, val email: String? = null, var flow: String? = null, val ivCheck: Boolean? = null, var users: List? = null ) { - - data class SocksUsersBean( var user: String = "", var pass: String = "", - var level: Int = DEFAULT_LEVEL + var level: Int = AppConfig.DEFAULT_LEVEL ) } data class Response(var type: String) - data class WireGuardBean(var publicKey: String = "", - var endpoint: String = "", - var keepAlive: Int = 0) + data class WireGuardBean( + var publicKey: String = "", + var preSharedKey: String = "", + var endpoint: String = "", + var keepAlive: Int = 0 + ) } data class StreamSettingsBean( - var network: String = DEFAULT_NETWORK, - var security: String = "", + var network: String = AppConfig.DEFAULT_NETWORK, + var security: String? = null, var tcpSettings: TcpSettingsBean? = null, 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, var realitySettings: TlsSettingsBean? = null, var grpcSettings: GrpcSettingsBean? = null, + var hy2steriaSettings: Hy2steriaSettingsBean? = null, val dsSettings: Any? = null, var sockopt: SockoptBean? = null ) { @@ -218,7 +266,7 @@ data class V2rayConfig( } data class WsSettingsBean( - var path: String = "", + var path: String? = null, var headers: HeadersBean = HeadersBean(), val maxEarlyData: Int? = null, val useBrowserForwarding: Boolean? = null, @@ -228,21 +276,21 @@ data class V2rayConfig( } data class HttpupgradeSettingsBean( - var path: String = "", - var host: String = "", + var path: String? = null, + var host: String? = null, val acceptProxyProtocol: Boolean? = null ) - data class SplithttpSettingsBean( - var path: String = "", - var host: String = "", - val maxUploadSize: Int? = null, - val maxConcurrentUploads: Int? = null + data class XhttpSettingsBean( + var path: String? = null, + var host: String? = null, + var mode: String? = null, + var extra: Any? = null, ) data class HttpSettingsBean( var host: List = ArrayList(), - var path: String = "" + var path: String? = null ) data class SockoptBean( @@ -256,7 +304,7 @@ data class V2rayConfig( data class TlsSettingsBean( var allowInsecure: Boolean = false, - var serverName: String = "", + var serverName: String? = null, val alpn: List? = null, val minVersion: String? = null, val maxVersion: String? = null, @@ -289,33 +337,52 @@ data class V2rayConfig( var health_check_timeout: Int? = null ) + data class Hy2steriaSettingsBean( + var password: String? = null, + var use_udp_extension: Boolean? = true, + var congestion: Hy2CongestionBean? = null + ) { + data class Hy2CongestionBean( + var type: String? = "bbr", + var up_mbps: Int? = null, + var down_mbps: Int? = null, + ) + } + 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 = "" - network = transport + ): String? { + var sni: String? = null + network = if (transport.isEmpty()) NetworkType.TCP.type else transport when (network) { - "tcp" -> { + NetworkType.TCP.type -> { val tcpSetting = TcpSettingsBean() - if (headerType == HTTP) { - tcpSetting.header.type = HTTP + if (headerType == AppConfig.HEADER_TYPE_HTTP) { + tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) { val requestObj = TcpSettingsBean.HeaderBean.RequestBean() - requestObj.headers.Host = (host.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() } - requestObj.path = (path.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() } + requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() } + requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() } tcpSetting.header.request = requestObj - sni = requestObj.headers.Host?.getOrNull(0) ?: sni + sni = requestObj.headers.Host?.getOrNull(0) } } else { tcpSetting.header.type = "none" - sni = host.orEmpty() + sni = host } tcpSettings = tcpSetting } - "kcp" -> { + NetworkType.KCP.type -> { val kcpsetting = KcpSettingsBean() kcpsetting.header.type = headerType ?: "none" if (seed.isNullOrEmpty()) { @@ -326,35 +393,35 @@ data class V2rayConfig( kcpSettings = kcpsetting } - "ws" -> { + NetworkType.WS.type -> { val wssetting = WsSettingsBean() wssetting.headers.Host = host.orEmpty() - sni = wssetting.headers.Host + sni = host wssetting.path = path ?: "/" wsSettings = wssetting } - "httpupgrade" -> { + NetworkType.HTTP_UPGRADE.type -> { val httpupgradeSetting = HttpupgradeSettingsBean() httpupgradeSetting.host = host.orEmpty() - sni = httpupgradeSetting.host + sni = host httpupgradeSetting.path = path ?: "/" httpupgradeSettings = httpupgradeSetting } - "splithttp" -> { - val splithttpSetting = SplithttpSettingsBean() - splithttpSetting.host = host.orEmpty() - sni = splithttpSetting.host - splithttpSetting.path = path ?: "/" - splithttpSettings = splithttpSetting + NetworkType.XHTTP.type -> { + val xhttpSetting = XhttpSettingsBean() + xhttpSetting.host = host.orEmpty() + sni = 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 + h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() } + sni = h2Setting.host.getOrNull(0) h2Setting.path = path ?: "/" httpSettings = h2Setting } @@ -367,14 +434,14 @@ data class V2rayConfig( quicSettings = quicsetting } - "grpc" -> { + NetworkType.GRPC.type -> { val grpcSetting = GrpcSettingsBean() grpcSetting.multiMode = mode == "multi" grpcSetting.serviceName = serviceName.orEmpty() grpcSetting.authority = authority.orEmpty() grpcSetting.idle_timeout = 60 grpcSetting.health_check_timeout = 20 - sni = authority.orEmpty() + sni = authority grpcSettings = grpcSetting } } @@ -382,23 +449,30 @@ 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 + security = if (streamSecurity.isEmpty()) null else streamSecurity + if (security == null) return val tlsSetting = TlsSettingsBean( allowInsecure = allowInsecure, - serverName = sni, - fingerprint = fingerprint, + serverName = if (sni.isNullOrEmpty()) null else sni, + fingerprint = if (fingerprint.isNullOrEmpty()) null else fingerprint, alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() }, - publicKey = publicKey, - shortId = shortId, - spiderX = spiderX + publicKey = if (publicKey.isNullOrEmpty()) null else publicKey, + shortId = if (shortId.isNullOrEmpty()) null else shortId, + spiderX = if (spiderX.isNullOrEmpty()) null else spiderX, ) - if (security == TLS) { + if (security == AppConfig.TLS) { tlsSettings = tlsSetting realitySettings = null - } else if (security == REALITY) { + } else if (security == AppConfig.REALITY) { tlsSettings = null realitySettings = tlsSetting } @@ -407,23 +481,25 @@ data class V2rayConfig( data class MuxBean( var enabled: Boolean, - var concurrency: Int = 8, - var xudpConcurrency: Int = 8, - var xudpProxyUDP443: String = "", + var concurrency: Int? = null, + var xudpConcurrency: Int? = null, + var xudpProxyUDP443: String? = null, ) fun getServerAddress(): String? { 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 } @@ -432,29 +508,40 @@ 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 } + fun getServerAddressAndPort(): String { + val address = getServerAddress().orEmpty() + val port = getServerPort() + return Utils.getIpv6Address(address) + ":" + port + } + fun getPassword(): String? { if (protocol.equals(EConfigType.VMESS.name, true) || protocol.equals(EConfigType.VLESS.name, true) ) { - return settings?.vnext?.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?.first()?.password + } else if (protocol.equals(EConfigType.SOCKS.name, true) + || protocol.equals(EConfigType.HTTP.name, true) ) { - return settings?.servers?.get(0)?.password - } else if (protocol.equals(EConfigType.SOCKS.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 } @@ -463,14 +550,14 @@ 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 } } - fun getTransportSettingDetails(): List? { + fun getTransportSettingDetails(): List? { if (protocol.equals(EConfigType.VMESS.name, true) || protocol.equals(EConfigType.VLESS.name, true) || protocol.equals(EConfigType.TROJAN.name, true) @@ -478,16 +565,16 @@ 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, - tcpSetting.header.request?.headers?.Host?.joinToString().orEmpty(), - tcpSetting.header.request?.path?.joinToString().orEmpty() + tcpSetting.header.request?.headers?.Host?.joinToString(",").orEmpty(), + tcpSetting.header.request?.path?.joinToString(",").orEmpty() ) } - "kcp" -> { + NetworkType.KCP.type -> { val kcpSetting = streamSettings?.kcpSettings ?: return null listOf( kcpSetting.header.type, @@ -496,7 +583,7 @@ data class V2rayConfig( ) } - "ws" -> { + NetworkType.WS.type -> { val wsSetting = streamSettings?.wsSettings ?: return null listOf( "", @@ -505,7 +592,7 @@ data class V2rayConfig( ) } - "httpupgrade" -> { + NetworkType.HTTP_UPGRADE.type -> { val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null listOf( "", @@ -514,20 +601,20 @@ data class V2rayConfig( ) } - "splithttp" -> { - val splithttpSetting = streamSettings?.splithttpSettings ?: return null + 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( "", - h2Setting.host.joinToString(), + h2Setting.host.joinToString(","), h2Setting.path ) } @@ -541,7 +628,7 @@ data class V2rayConfig( ) } - "grpc" -> { + NetworkType.GRPC.type -> { val grpcSetting = streamSettings?.grpcSettings ?: return null listOf( if (grpcSetting.multiMode == true) "multi" else "gun", @@ -570,7 +657,8 @@ data class V2rayConfig( var port: Int? = null, var domains: List? = null, var expectIPs: List? = null, - val clientIp: String? = null + val clientIp: String? = null, + val skipFallback: Boolean? = null, ) } @@ -582,6 +670,7 @@ data class V2rayConfig( ) { data class RulesBean( + var type: String = "field", var ip: ArrayList? = null, var domain: ArrayList? = null, var outboundTag: String = "", @@ -640,4 +729,4 @@ data class V2rayConfig( .create() .toJson(this) } -} +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt similarity index 100% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt new file mode 100644 index 0000000000..a5e0da0b3d --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt @@ -0,0 +1,98 @@ +package com.v2ray.ang.extension + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.Bundle +import android.widget.Toast +import com.v2ray.ang.AngApplication +import me.drakeet.support.toast.ToastCompat +import org.json.JSONObject +import java.io.Serializable +import java.net.URI +import java.net.URLConnection + +val Context.v2RayApplication: AngApplication? + get() = applicationContext as? AngApplication + +fun Context.toast(message: Int) { + ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show() +} + +fun Context.toast(message: CharSequence) { + ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show() +} + +fun JSONObject.putOpt(pair: Pair) { + put(pair.first, pair.second) +} + +fun JSONObject.putOpt(pairs: Map) { + pairs.forEach { put(it.key, it.value) } +} + +const val THRESHOLD = 1000L +const val DIVISOR = 1024.0 + +fun Long.toSpeedString(): String = this.toTrafficString() + "/s" + +fun Long.toTrafficString(): String { + val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB") + var size = this.toDouble() + var unitIndex = 0 + while (size >= THRESHOLD && unitIndex < units.size - 1) { + size /= DIVISOR + unitIndex++ + } + return String.format("%.1f %s", size, units[unitIndex]) +} + +val URLConnection.responseLength: Long + get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + contentLengthLong + } else { + contentLength.toLong() + } + +val URI.idnHost: String + get() = host?.replace("[", "")?.replace("]", "").orEmpty() + +fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "") + +fun String.toLongEx(): Long = toLongOrNull() ?: 0 + +fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + callback() + if (onetime) context.unregisterReceiver(this) + } + }.apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(this, IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_REMOVED) + addDataScheme("package") + }, Context.RECEIVER_EXPORTED) + } else { + registerReceiver(this, IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_REMOVED) + addDataScheme("package") + }) + } + } + +inline fun Bundle.serializable(key: String): T? = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java) + else -> @Suppress("DEPRECATION") getSerializable(key) as? T +} + +inline fun Intent.serializable(key: String): T? = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java) + else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T +} + +fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty()) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/CustomFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/CustomFmt.kt new file mode 100644 index 0000000000..7a8738325e --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/CustomFmt.kt @@ -0,0 +1,21 @@ +package com.v2ray.ang.fmt + +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.util.JsonUtil + +object CustomFmt : FmtBase() { + fun parse(str: String): ProfileItem? { + val config = ProfileItem.create(EConfigType.CUSTOM) + + val fullConfig = JsonUtil.fromJson(str, V2rayConfig::class.java) + val outbound = fullConfig.getProxyOutbound() + + config.remarks = fullConfig?.remarks ?: System.currentTimeMillis().toString() + config.server = outbound?.getServerAddress() + config.serverPort = outbound?.getServerPort().toString() + + return config + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt new file mode 100644 index 0000000000..aab337b93b --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt @@ -0,0 +1,125 @@ +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 +import java.net.URI + +open class FmtBase { + fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap?): String { + val query = if (dicQuery != null) + ("?" + dicQuery.toList().joinToString( + separator = "&", + transform = { it.first + "=" + Utils.urlEncode(it.second) })) + else "" + + val url = String.format( + "%s@%s:%s", + Utils.urlEncode(userInfo ?: ""), + Utils.getIpv6Address(config.server), + config.serverPort + ) + + return "${url}${query}#${Utils.urlEncode(config.remarks)}" + } + + fun getQueryParam(uri: URI): Map { + return uri.rawQuery.split("&") + .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } + } + + fun getItemFormQuery(config: ProfileItem, queryParam: Map, allowInsecure: Boolean) { + config.network = queryParam["type"] ?: NetworkType.TCP.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"] + if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) { + config.security = null + } + 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 { + val dicQuery = HashMap() + 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() } + config.fingerPrint.let { if (it.isNotNullEmpty()) dicQuery["fp"] = it.orEmpty() } + config.publicKey.let { if (it.isNotNullEmpty()) dicQuery["pbk"] = it.orEmpty() } + config.shortId.let { if (it.isNotNullEmpty()) dicQuery["sid"] = it.orEmpty() } + config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() } + config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() } + + val networkType = NetworkType.fromString(config.network) + dicQuery["type"] = networkType.type + + when (networkType) { + NetworkType.TCP -> { + dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty() + config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() } + } + + NetworkType.KCP -> { + dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty() + config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() } + } + + 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() } + } + + 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() } + } + + 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() } + } + + 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() } + } + } + + return dicQuery + } + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt new file mode 100644 index 0000000000..0c85b1717a --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt @@ -0,0 +1,28 @@ +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.isNotNullEmpty +import kotlin.text.orEmpty + +object HttpFmt : FmtBase() { + fun toOutbound(profileItem: ProfileItem): OutboundBean? { + val outboundBean = OutboundBean.create(EConfigType.HTTP) + + outboundBean?.settings?.servers?.first()?.let { server -> + server.address = profileItem.server.orEmpty() + server.port = profileItem.serverPort.orEmpty().toInt() + if (profileItem.username.isNotNullEmpty()) { + val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() + socksUsersBean.user = profileItem.username.orEmpty() + socksUsersBean.pass = profileItem.password.orEmpty() + server.users = listOf(socksUsersBean) + } + } + + return outboundBean + } + + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt new file mode 100644 index 0000000000..5b15007aea --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt @@ -0,0 +1,132 @@ +package com.v2ray.ang.fmt + +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.LOOPBACK +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.Hysteria2Bean +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.V2rayConfig.OutboundBean +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.extension.isNotNullEmpty +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.util.Utils +import java.net.URI + +object Hysteria2Fmt : FmtBase() { + fun parse(str: String): ProfileItem? { + var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) + val config = ProfileItem.create(EConfigType.HYSTERIA2) + + val uri = URI(Utils.fixIllegalUrl(str)) + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) + config.server = uri.idnHost + config.serverPort = uri.port.toString() + config.password = uri.userInfo + config.security = AppConfig.TLS + + if (!uri.rawQuery.isNullOrEmpty()) { + val queryParam = getQueryParam(uri) + + config.security = queryParam["security"] ?: AppConfig.TLS + config.insecure = if (queryParam["insecure"].isNullOrEmpty()) { + allowInsecure + } else { + queryParam["insecure"].orEmpty() == "1" + } + config.sni = queryParam["sni"] + config.alpn = queryParam["alpn"] + + config.obfsPassword = queryParam["obfs-password"] + config.portHopping = queryParam["mport"] + config.pinSHA256 = queryParam["pinSHA256"] + + } + + return config + } + + fun toUri(config: ProfileItem): String { + val dicQuery = HashMap() + + config.security.let { if (it != null) dicQuery["security"] = it } + config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() } + config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() } + config.insecure.let { dicQuery["insecure"] = if (it == true) "1" else "0" } + + if (config.obfsPassword.isNotNullEmpty()) { + dicQuery["obfs"] = "salamander" + dicQuery["obfs-password"] = config.obfsPassword.orEmpty() + } + if (config.portHopping.isNotNullEmpty()) { + dicQuery["mport"] = config.portHopping.orEmpty() + } + if (config.pinSHA256.isNotNullEmpty()) { + dicQuery["pinSHA256"] = config.pinSHA256.orEmpty() + } + + return toUri(config, config.password, dicQuery) + } + + fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? { + + val obfs = if (config.obfsPassword.isNullOrEmpty()) null else + Hysteria2Bean.ObfsBean( + type = "salamander", + salamander = Hysteria2Bean.ObfsBean.SalamanderBean( + password = config.obfsPassword + ) + ) + + val transport = if (config.portHopping.isNullOrEmpty()) null else + Hysteria2Bean.TransportBean( + type = "udp", + udp = Hysteria2Bean.TransportBean.TransportUdpBean( + //hopInterval = (config.portHoppingInterval ?: "30") + "s" + hopInterval = if(config.portHoppingInterval.isNullOrEmpty()) { + "30s" + }else{ + (config.portHoppingInterval) + "s" + } + ) + ) + + val bandwidth = if (config.bandwidthDown.isNullOrEmpty() || config.bandwidthUp.isNullOrEmpty()) null else + Hysteria2Bean.BandwidthBean( + down = config.bandwidthDown, + up = config.bandwidthUp, + ) + + val server = + if (config.portHopping.isNullOrEmpty()) + config.getServerAddressAndPort() + else + Utils.getIpv6Address(config.server) + ":" + config.portHopping + + val bean = Hysteria2Bean( + server = server, + auth = config.password, + obfs = obfs, + transport = transport, + bandwidth = bandwidth, + socks5 = Hysteria2Bean.Socks5Bean( + listen = "$LOOPBACK:${socksPort}", + ), + http = Hysteria2Bean.Socks5Bean( + listen = "$LOOPBACK:${socksPort}", + ), + tls = Hysteria2Bean.TlsBean( + sni = config.sni ?: config.server, + insecure = config.insecure, + pinSHA256 = if (config.pinSHA256.isNullOrEmpty()) null else config.pinSHA256 + ) + ) + return bean + } + + + fun toOutbound(profileItem: ProfileItem): OutboundBean? { + val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2) + return outboundBean + } + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt new file mode 100644 index 0000000000..c4edc5bdb1 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt @@ -0,0 +1,139 @@ +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"]?.contains("obfs=http") == true) { + val queryPairs = HashMap() + for (pair in queryParam["plugin"]?.split(";") ?: listOf()) { + val idx = pair.split("=") + if (idx.count() == 2) { + queryPairs.put(idx.first(), idx.last()) + } + } + config.network = NetworkType.TCP.type + config.headerType = "http" + config.host = queryPairs["obfs-host"] + config.path = queryPairs["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 + } + + val sni = outboundBean?.streamSettings?.populateTransportSettings( + profileItem.network.orEmpty(), + profileItem.headerType, + profileItem.host, + profileItem.path, + profileItem.seed, + profileItem.quicSecurity, + profileItem.quicKey, + profileItem.mode, + profileItem.serviceName, + profileItem.authority, + ) + + outboundBean?.streamSettings?.populateTlsSettings( + profileItem.security.orEmpty(), + profileItem.insecure == true, + if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni, + profileItem.fingerPrint, + profileItem.alpn, + profileItem.publicKey, + profileItem.shortId, + profileItem.spiderX, + ) + + return outboundBean + } + + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt new file mode 100644 index 0000000000..b610a9daf2 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt @@ -0,0 +1,62 @@ +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.extension.isNotNullEmpty +import com.v2ray.ang.util.Utils +import java.net.URI +import kotlin.text.orEmpty + +object SocksFmt : FmtBase() { + fun parse(str: String): ProfileItem? { + val config = ProfileItem.create(EConfigType.SOCKS) + + 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(":", limit = 2) + if (result.count() == 2) { + config.username = result.first() + config.password = result.last() + } + } + + return config + } + + fun toUri(config: ProfileItem): String { + val pw = + if (config.username.isNotNullEmpty()) + "${config.username}:${config.password}" + else + ":" + + return toUri(config, Utils.encode(pw), null) + } + + fun toOutbound(profileItem: ProfileItem): OutboundBean? { + val outboundBean = OutboundBean.create(EConfigType.SOCKS) + + outboundBean?.settings?.servers?.first()?.let { server -> + server.address = profileItem.server.orEmpty() + server.port = profileItem.serverPort.orEmpty().toInt() + if (profileItem.username.isNotNullEmpty()) { + val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() + socksUsersBean.user = profileItem.username.orEmpty() + socksUsersBean.pass = profileItem.password.orEmpty() + server.users = listOf(socksUsersBean) + } + } + + return outboundBean + } + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt new file mode 100644 index 0000000000..7139ada061 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt @@ -0,0 +1,81 @@ +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 +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.util.Utils +import java.net.URI +import kotlin.text.orEmpty + +object TrojanFmt : FmtBase() { + fun parse(str: String): ProfileItem? { + var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) + val config = ProfileItem.create(EConfigType.TROJAN) + + val uri = URI(Utils.fixIllegalUrl(str)) + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) + config.server = uri.idnHost + config.serverPort = uri.port.toString() + 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) + + getItemFormQuery(config, queryParam, allowInsecure) + config.security = queryParam["security"] ?: AppConfig.TLS + } + + return config + } + + fun toUri(config: ProfileItem): String { + val dicQuery = getQueryDic(config) + + return toUri(config, config.password, dicQuery) + } + + fun toOutbound(profileItem: ProfileItem): OutboundBean? { + val outboundBean = OutboundBean.create(EConfigType.TROJAN) + + outboundBean?.settings?.servers?.first()?.let { server -> + server.address = profileItem.server.orEmpty() + server.port = profileItem.serverPort.orEmpty().toInt() + server.password = profileItem.password + server.flow = profileItem.flow + } + + val sni = outboundBean?.streamSettings?.populateTransportSettings( + profileItem.network.orEmpty(), + profileItem.headerType, + profileItem.host, + profileItem.path, + profileItem.seed, + profileItem.quicSecurity, + profileItem.quicKey, + profileItem.mode, + profileItem.serviceName, + profileItem.authority, + ) + + outboundBean?.streamSettings?.populateTlsSettings( + profileItem.security.orEmpty(), + profileItem.insecure == true, + if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni, + profileItem.fingerPrint, + profileItem.alpn, + profileItem.publicKey, + profileItem.shortId, + profileItem.spiderX, + ) + + return outboundBean + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt new file mode 100644 index 0000000000..a4ca34c51e --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt @@ -0,0 +1,83 @@ +package com.v2ray.ang.fmt + +import com.v2ray.ang.AppConfig +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.handler.MmkvManager +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.Utils +import java.net.URI + +object VlessFmt : FmtBase() { + + fun parse(str: String): ProfileItem? { + var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) + val config = ProfileItem.create(EConfigType.VLESS) + + val uri = URI(Utils.fixIllegalUrl(str)) + if (uri.rawQuery.isNullOrEmpty()) return null + val queryParam = getQueryParam(uri) + + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) + config.server = uri.idnHost + config.serverPort = uri.port.toString() + config.password = uri.userInfo + config.method = queryParam["encryption"] ?: "none" + + getItemFormQuery(config, queryParam, allowInsecure) + + return config + } + + fun toUri(config: ProfileItem): String { + val dicQuery = getQueryDic(config) + dicQuery["encryption"] = config.method ?: "none" + + return toUri(config, config.password, dicQuery) + } + + + fun toOutbound(profileItem: ProfileItem): OutboundBean? { + val outboundBean = OutboundBean.create(EConfigType.VLESS) + + outboundBean?.settings?.vnext?.first()?.let { vnext -> + vnext.address = profileItem.server.orEmpty() + vnext.port = profileItem.serverPort.orEmpty().toInt() + vnext.users[0].id = profileItem.password.orEmpty() + vnext.users[0].encryption = profileItem.method + vnext.users[0].flow = profileItem.flow + } + + val sni = outboundBean?.streamSettings?.populateTransportSettings( + profileItem.network.orEmpty(), + profileItem.headerType, + profileItem.host, + profileItem.path, + profileItem.seed, + profileItem.quicSecurity, + profileItem.quicKey, + profileItem.mode, + profileItem.serviceName, + profileItem.authority, + ) + outboundBean?.streamSettings?.xhttpSettings?.mode = profileItem.xhttpMode + outboundBean?.streamSettings?.xhttpSettings?.extra = JsonUtil.parseString(profileItem.xhttpExtra) + + outboundBean?.streamSettings?.populateTlsSettings( + profileItem.security.orEmpty(), + profileItem.insecure == true, + if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni, + profileItem.fingerPrint, + profileItem.alpn, + profileItem.publicKey, + profileItem.shortId, + profileItem.spiderX, + ) + + return outboundBean + } + + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt new file mode 100644 index 0000000000..72a5868aeb --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt @@ -0,0 +1,183 @@ +package com.v2ray.ang.fmt + +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 +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.extension.isNotNullEmpty +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.Utils +import java.net.URI +import kotlin.text.orEmpty + +object VmessFmt : FmtBase() { + fun parse(str: String): ProfileItem? { + if (str.indexOf('?') > 0 && str.indexOf('&') > 0) { + return parseVmessStd(str) + } + + var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) + val config = ProfileItem.create(EConfigType.VMESS) + + var result = str.replace(EConfigType.VMESS.protocolScheme, "") + result = Utils.decode(result) + if (TextUtils.isEmpty(result)) { + Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed") + return null + } + val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java) + // Although VmessQRCode fields are non null, looks like Gson may still create null fields + if (TextUtils.isEmpty(vmessQRCode.add) + || TextUtils.isEmpty(vmessQRCode.port) + || TextUtils.isEmpty(vmessQRCode.id) + || TextUtils.isEmpty(vmessQRCode.net) + ) { + Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol") + return null + } + + config.remarks = vmessQRCode.ps + config.server = vmessQRCode.add + config.serverPort = vmessQRCode.port + config.password = vmessQRCode.id + config.method = if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy + + config.network = vmessQRCode.net ?: NetworkType.TCP.type + config.headerType = vmessQRCode.type + config.host = vmessQRCode.host + config.path = vmessQRCode.path + + when (NetworkType.fromString(config.network)) { + NetworkType.KCP -> { + config.seed = vmessQRCode.path + } + +// NetworkType.QUIC -> { +// config.quicSecurity = vmessQRCode.host +// config.quicKey = vmessQRCode.path +// } + + 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 + config.fingerPrint = vmessQRCode.fp + config.alpn = vmessQRCode.alpn + + return config + } + + fun toUri(config: ProfileItem): String { + val vmessQRCode = VmessQRCode() + + vmessQRCode.v = "2" + vmessQRCode.ps = config.remarks + vmessQRCode.add = config.server.orEmpty() + vmessQRCode.port = config.serverPort.orEmpty() + vmessQRCode.id = config.password.orEmpty() + vmessQRCode.scy = config.method.orEmpty() + vmessQRCode.aid = "0" + + vmessQRCode.net = config.network.orEmpty() + vmessQRCode.type = config.headerType.orEmpty() + when (NetworkType.fromString(config.network)) { + NetworkType.KCP -> { + vmessQRCode.path = config.seed.orEmpty() + } + +// NetworkType.QUIC -> { +// vmessQRCode.host = config.quicSecurity.orEmpty() +// vmessQRCode.path = config.quicKey.orEmpty() +// } + + 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() } + + vmessQRCode.tls = config.security.orEmpty() + vmessQRCode.sni = config.sni.orEmpty() + vmessQRCode.fp = config.fingerPrint.orEmpty() + vmessQRCode.alpn = config.alpn.orEmpty() + + val json = JsonUtil.toJson(vmessQRCode) + return Utils.encode(json) + } + + fun parseVmessStd(str: String): ProfileItem? { + val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) + val config = ProfileItem.create(EConfigType.VMESS) + + val uri = URI(Utils.fixIllegalUrl(str)) + if (uri.rawQuery.isNullOrEmpty()) return null + val queryParam = getQueryParam(uri) + + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) + config.server = uri.idnHost + config.serverPort = uri.port.toString() + config.password = uri.userInfo + config.method = AppConfig.DEFAULT_SECURITY + + getItemFormQuery(config, queryParam, allowInsecure) + + return config + } + + + fun toOutbound(profileItem: ProfileItem): OutboundBean? { + val outboundBean = OutboundBean.create(EConfigType.VMESS) + + outboundBean?.settings?.vnext?.first()?.let { vnext -> + vnext.address = profileItem.server.orEmpty() + vnext.port = profileItem.serverPort.orEmpty().toInt() + vnext.users[0].id = profileItem.password.orEmpty() + vnext.users[0].security = profileItem.method + } + + val sni = outboundBean?.streamSettings?.populateTransportSettings( + profileItem.network.orEmpty(), + profileItem.headerType, + profileItem.host, + profileItem.path, + profileItem.seed, + profileItem.quicSecurity, + profileItem.quicKey, + profileItem.mode, + profileItem.serviceName, + profileItem.authority, + ) + + outboundBean?.streamSettings?.populateTlsSettings( + profileItem.security.orEmpty(), + profileItem.insecure == true, + if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni, + profileItem.fingerPrint, + profileItem.alpn, + null, + null, + null + ) + + return outboundBean + } + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt new file mode 100644 index 0000000000..6c2f7bf75a --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt @@ -0,0 +1,192 @@ +package com.v2ray.ang.fmt + +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 +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.extension.isNotNullEmpty +import com.v2ray.ang.util.Utils +import java.net.URI +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlin.text.orEmpty + +object WireguardFmt : FmtBase() { + fun parse(str: String): ProfileItem? { + val config = ProfileItem.create(EConfigType.WIREGUARD) + + val uri = URI(Utils.fixIllegalUrl(str)) + if (uri.rawQuery.isNullOrEmpty()) return null + val queryParam = getQueryParam(uri) + + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) + config.server = uri.idnHost + config.serverPort = uri.port.toString() + + 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") + + config.keepAlive = Utils.parseInt(queryParam["keepalive"] ?: AppConfig.WIREGUARD_keep_alive) + config.wnoise = queryParam["wnoise"] ?: AppConfig.WIREGUARD_wnoise + config.wnoisecount = queryParam["wnoisecount"] ?: AppConfig.WIREGUARD_wnoisecount + config.wnoisedelay = queryParam["wnoisedelay"] ?: AppConfig.WIREGUARD_wnoisedelay + config.wpayloadsize = queryParam["wpayloadsize"] ?: AppConfig.WIREGUARD_wpayloadsize + + return config + } + + fun parseWireguardConfFile(str: String): ProfileItem? { + val config = ProfileItem.create(EConfigType.WIREGUARD) + + val interfaceParams: MutableMap = mutableMapOf() + val peerParams: MutableMap = mutableMapOf() + var my_remark = "" + + var currentSection: String? = null + + str.lines().forEach { line -> + val trimmedLine = line.trim() + + + if (trimmedLine.isEmpty()) { + return@forEach + } + + if(trimmedLine.startsWith("#")){ + my_remark = if(trimmedLine.length>30){ + trimmedLine.substring(1,30).trim() + }else{ + trimmedLine.substring(1).trim() + } + return@forEach + } + + when { + trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface" + trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer" + 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 + } + } + } + } + } + } + + if(my_remark.isEmpty()){ + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US) + val currentDate = dateFormat.format(Date()) + config.remarks = "WG $currentDate" + }else{ + config.remarks = my_remark + } + + + config.secretKey = interfaceParams["privatekey"].orEmpty() + 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" + + config.keepAlive = Utils.parseInt(peerParams["keepalive"] ?: AppConfig.WIREGUARD_keep_alive) + config.wnoise = interfaceParams["wnoise"] ?: AppConfig.WIREGUARD_wnoise + config.wnoisecount = interfaceParams["wnoisecount"] ?: AppConfig.WIREGUARD_wnoisecount + config.wnoisedelay = interfaceParams["wnoisedelay"] ?: AppConfig.WIREGUARD_wnoisedelay + config.wpayloadsize = interfaceParams["wpayloadsize"] ?: AppConfig.WIREGUARD_wpayloadsize + + 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}" + peer.keepAlive = profileItem.keepAlive ?: Utils.parseInt(AppConfig.WIREGUARD_keep_alive) + } + wireguard.mtu = profileItem.mtu + + try { + wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() } + }catch (_:Exception){ + wireguard.reserved = listOf(0,0,0) + } + + wireguard.wnoise = profileItem.wnoise + wireguard.wnoisecount = profileItem.wnoisecount + wireguard.wnoisedelay = profileItem.wnoisedelay + wireguard.wpayloadsize = profileItem.wpayloadsize + + } + + return outboundBean + } + + fun toUri(config: ProfileItem): String { + val dicQuery = HashMap() + + dicQuery["publickey"] = config.publicKey.orEmpty() + + if (config.reserved != null) { + dicQuery["reserved"] = Utils.removeWhiteSpace(config.reserved).orEmpty() + } + dicQuery["address"] = Utils.removeWhiteSpace(config.localAddress).orEmpty() + + if (config.mtu != null) { + dicQuery["mtu"] = config.mtu.toString() + } + + if (config.preSharedKey.isNotNullEmpty()) { + dicQuery["presharedkey"] = Utils.removeWhiteSpace(config.preSharedKey).orEmpty() + } + + //---------- + if (config.keepAlive != null) { + dicQuery["keepalive"] = config.keepAlive.toString() + } + if (config.wnoise.isNotNullEmpty()) { + dicQuery["wnoise"] = Utils.removeWhiteSpace(config.wnoise).orEmpty() + } + if (config.wnoisecount.isNotNullEmpty()) { + dicQuery["wnoisecount"] = Utils.removeWhiteSpace(config.wnoisecount).orEmpty() + } + if (config.wnoisedelay.isNotNullEmpty()) { + dicQuery["wnoisedelay"] = Utils.removeWhiteSpace(config.wnoisedelay).orEmpty() + } + if (config.wpayloadsize.isNotNullEmpty()) { + dicQuery["wpayloadsize"] = Utils.removeWhiteSpace(config.wpayloadsize).orEmpty() + } + //--------- + + return toUri(config, config.secretKey, dicQuery) + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt new file mode 100644 index 0000000000..32b19f00d6 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt @@ -0,0 +1,406 @@ +package com.v2ray.ang.handler + +import android.content.Context +import android.graphics.Bitmap +import android.text.TextUtils +import android.util.Log +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.HY2 +import com.v2ray.ang.R +import com.v2ray.ang.dto.* +import com.v2ray.ang.fmt.CustomFmt +import com.v2ray.ang.fmt.Hysteria2Fmt +import com.v2ray.ang.fmt.ShadowsocksFmt +import com.v2ray.ang.fmt.SocksFmt +import com.v2ray.ang.fmt.TrojanFmt +import com.v2ray.ang.fmt.VlessFmt +import com.v2ray.ang.fmt.VmessFmt +import com.v2ray.ang.fmt.WireguardFmt +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.QRCodeDecoder +import com.v2ray.ang.util.Utils +import java.net.URI + +object AngConfigManager { + /** + * parse config form qrcode or... + */ + private fun parseConfig( + str: String?, + subid: String, + subItem: SubscriptionItem?, + removedSelectedServer: ProfileItem? + ): Int { + try { + if (str == null || TextUtils.isEmpty(str)) { + return R.string.toast_none_data + } + + val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) { + VmessFmt.parse(str) + } else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) { + ShadowsocksFmt.parse(str) + } else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) { + SocksFmt.parse(str) + } else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) { + TrojanFmt.parse(str) + } else if (str.startsWith(EConfigType.VLESS.protocolScheme)) { + VlessFmt.parse(str) + } else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) { + WireguardFmt.parse(str) + } else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme) || str.startsWith(HY2)) { + Hysteria2Fmt.parse(str) + } else { + null + } + + if (config == null) { + return R.string.toast_incorrect_protocol + } + //filter + if (subItem?.filter != null && subItem.filter?.isNotEmpty() == true && config.remarks.isNotEmpty()) { + val matched = Regex(pattern = subItem.filter ?: "") + .containsMatchIn(input = config.remarks) + if (!matched) return -1 + } + + config.subscriptionId = subid + val guid = MmkvManager.encodeServerConfig("", config) + if (removedSelectedServer != null && + config.server == removedSelectedServer.server && config.serverPort == removedSelectedServer.serverPort + ) { + MmkvManager.setSelectServer(guid) + } + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + /** + * share config + */ + private fun shareConfig(guid: String): String { + try { + val config = MmkvManager.decodeServerConfig(guid) ?: return "" + + return config.configType.protocolScheme + when (config.configType) { + EConfigType.VMESS -> VmessFmt.toUri(config) + EConfigType.CUSTOM -> "" + EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config) + EConfigType.SOCKS -> SocksFmt.toUri(config) + EConfigType.HTTP -> "" + EConfigType.VLESS -> VlessFmt.toUri(config) + EConfigType.TROJAN -> TrojanFmt.toUri(config) + EConfigType.WIREGUARD -> WireguardFmt.toUri(config) + EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config) + } + } catch (e: Exception) { + e.printStackTrace() + return "" + } + } + + /** + * share2Clipboard + */ + fun share2Clipboard(context: Context, guid: String): Int { + try { + val conf = shareConfig(guid) + if (TextUtils.isEmpty(conf)) { + return -1 + } + + Utils.setClipboard(context, conf) + + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + /** + * share2Clipboard + */ + fun shareNonCustomConfigsToClipboard(context: Context, serverList: List): Int { + try { + val sb = StringBuilder() + for (guid in serverList) { + val url = shareConfig(guid) + if (TextUtils.isEmpty(url)) { + continue + } + sb.append(url) + sb.appendLine() + } + if (sb.count() > 0) { + Utils.setClipboard(context, sb.toString()) + } + return sb.lines().count() + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + } + + /** + * share2QRCode + */ + fun share2QRCode(guid: String): Bitmap? { + try { + val conf = shareConfig(guid) + if (TextUtils.isEmpty(conf)) { + return null + } + return QRCodeDecoder.createQRCode(conf) + + } catch (e: Exception) { + e.printStackTrace() + return null + } + } + + /** + * shareFullContent2Clipboard + */ + fun shareFullContent2Clipboard(context: Context, guid: String?): Int { + try { + if (guid == null) return -1 + val result = V2rayConfigManager.getV2rayConfig(context, guid) + if (result.status) { + val config = MmkvManager.decodeServerConfig(guid) + if (config?.configType == EConfigType.HYSTERIA2) { + val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0)) + val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) + Utils.setClipboard(context, JsonUtil.toJsonPretty(hy2Config) + "\n" + result.content) + return 0 + } + Utils.setClipboard(context, result.content) + } else { + return -1 + } + } catch (e: Exception) { + e.printStackTrace() + return -1 + } + return 0 + } + + fun importBatchConfig(server: String?, subid: String, append: Boolean): Pair { + var count = parseBatchConfig(Utils.decode(server), subid, append) + if (count <= 0) { + count = parseBatchConfig(server, subid, append) + } + if (count <= 0) { + count = parseCustomConfigServer(server, subid) + } + + var countSub = parseBatchSubscription(server) + if (countSub <= 0) { + countSub = parseBatchSubscription(Utils.decode(server)) + } + if (countSub > 0) { + updateConfigViaSubAll() + } + + return count to countSub + } + + fun parseBatchSubscription(servers: String?): Int { + try { + if (servers == null) { + return 0 + } + + var count = 0 + servers.lines() + .distinct() + .forEach { str -> + if (Utils.isValidSubUrl(str)) { + count += importUrlAsSubscription(str) + } + } + return count + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int { + try { + if (servers == null) { + return 0 + } + val removedSelectedServer = + if (!TextUtils.isEmpty(subid) && !append) { + MmkvManager.decodeServerConfig( + MmkvManager.getSelectServer().orEmpty() + )?.let { + if (it.subscriptionId == subid) { + return@let it + } + return@let null + } + } else { + null + } + if (!append) { + MmkvManager.removeServerViaSubid(subid) + } + + val subItem = MmkvManager.decodeSubscription(subid) + var count = 0 + servers.lines() + .distinct() + .reversed() + .forEach { + val resId = parseConfig(it, subid, subItem, removedSelectedServer) + if (resId == 0) { + count++ + } + } + return count + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } + + fun parseCustomConfigServer(server: String?, subid: String): Int { + if (server == null) { + return 0 + } + if (server.contains("inbounds") + && server.contains("outbounds") + && server.contains("routing") + ) { + try { + val serverList: Array = + JsonUtil.fromJson(server, Array::class.java) + + if (serverList.isNotEmpty()) { + var count = 0 + for (srv in serverList.reversed()) { + val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue + config.subscriptionId = subid + val key = MmkvManager.encodeServerConfig("", config) + MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "") + count += 1 + } + return count + } + } catch (e: Exception) { + e.printStackTrace() + } + + try { + // For compatibility + val config = CustomFmt.parse(server) ?: return 0 + config.subscriptionId = subid + val key = MmkvManager.encodeServerConfig("", config) + MmkvManager.encodeServerRaw(key, server) + return 1 + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } else if (server.startsWith("[Interface]") && server.contains("[Peer]")) { + try { + val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol + val key = MmkvManager.encodeServerConfig("", config) + MmkvManager.encodeServerRaw(key, server) + return 1 + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } else { + return 0 + } + } + + fun updateConfigViaSubAll(): Int { + var count = 0 + try { + MmkvManager.decodeSubscriptions().forEach { + count += updateConfigViaSub(it) + } + } catch (e: Exception) { + e.printStackTrace() + return 0 + } + return count + } + + fun updateConfigViaSub(it: Pair): Int { + try { + if (TextUtils.isEmpty(it.first) + || TextUtils.isEmpty(it.second.remarks) + || TextUtils.isEmpty(it.second.url) + ) { + return 0 + } + if (!it.second.enabled) { + return 0 + } + val url = Utils.idnToASCII(it.second.url) + if (!Utils.isValidUrl(url)) { + return 0 + } + Log.d(AppConfig.ANG_PACKAGE, url) + + var configText = try { + val httpPort = SettingsManager.getHttpPort() + Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort) + } catch (e: Exception) { + Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error, try……") + //e.printStackTrace() + "" + } + if (configText.isEmpty()) { + configText = try { + Utils.getUrlContentWithCustomUserAgent(url) + } catch (e: Exception) { + e.printStackTrace() + "" + } + } + if (configText.isEmpty()) { + return 0 + } + return parseConfigViaSub(configText, it.first, false) + } catch (e: Exception) { + e.printStackTrace() + return 0 + } + } + + private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int { + var count = parseBatchConfig(Utils.decode(server), subid, append) + if (count <= 0) { + count = parseBatchConfig(server, subid, append) + } + if (count <= 0) { + count = parseCustomConfigServer(server, subid) + } + return count + } + + private fun importUrlAsSubscription(url: String): Int { + val subscriptions = MmkvManager.decodeSubscriptions() + subscriptions.forEach { + if (it.second.url == url) { + return 0 + } + } + val uri = URI(Utils.fixIllegalUrl(url)) + val subItem = SubscriptionItem() + subItem.remarks = uri.fragment ?: "import sub" + subItem.url = url + MmkvManager.encodeSubscription("", subItem) + return 1 + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MigrateManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MigrateManager.kt new file mode 100644 index 0000000000..a6b6341217 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MigrateManager.kt @@ -0,0 +1,191 @@ +package com.v2ray.ang.handler + +import android.util.Log +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 +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.Utils + +object MigrateManager { + private const val ID_SERVER_CONFIG = "SERVER_CONFIG" + private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) } + + fun migrateServerConfig2Profile(): Boolean { + if (serverStorage.count().toInt() == 0) { + return false + } + val serverList = serverStorage.allKeys() ?: return false + Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count()) + + for (guid in serverList) { + var configOld = decodeServerConfigOld(guid) ?: continue + var config = decodeServerConfig(guid) + if (config != null) { + serverStorage.remove(guid) + continue + } + config = migrateServerConfig2ProfileSub(configOld) ?: continue + config.subscriptionId = configOld.subscriptionId + + MmkvManager.encodeServerConfig(guid, config) + + //check and remove old + decodeServerConfig(guid) ?: continue + serverStorage.remove(guid) + Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + config.remarks) + } + Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end") + return true + } + + private fun migrateServerConfig2ProfileSub(configOld: ServerConfig): ProfileItem? { + return when (configOld.getProxyOutbound()?.protocol) { + EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld) + EConfigType.VLESS.name.lowercase() -> migrate2ProfileCommon(configOld) + EConfigType.TROJAN.name.lowercase() -> migrate2ProfileCommon(configOld) + EConfigType.SHADOWSOCKS.name.lowercase() -> migrate2ProfileCommon(configOld) + + EConfigType.SOCKS.name.lowercase() -> migrate2ProfileSocks(configOld) + EConfigType.HTTP.name.lowercase() -> migrate2ProfileHttp(configOld) + EConfigType.WIREGUARD.name.lowercase() -> migrate2ProfileWireguard(configOld) + EConfigType.HYSTERIA2.name.lowercase() -> migrate2ProfileHysteria2(configOld) + + EConfigType.CUSTOM.name.lowercase() -> migrate2ProfileCustom(configOld) + + else -> null + } + } + + private fun migrate2ProfileCommon(configOld: ServerConfig): ProfileItem? { + val config = ProfileItem.create(configOld.configType) + + val outbound = configOld.getProxyOutbound() ?: return null + config.remarks = configOld.remarks + config.server = outbound.getServerAddress() + config.serverPort = outbound.getServerPort().toString() + config.method = outbound.getSecurityEncryption() + config.password = outbound.getPassword() + 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.headerType = transportDetails[0].orEmpty() + config.host = transportDetails[1].orEmpty() + config.path = transportDetails[2].orEmpty() + } + + config.seed = outbound?.streamSettings?.kcpSettings?.seed + config.quicSecurity = outbound?.streamSettings?.quicSettings?.security + config.quicKey = outbound?.streamSettings?.quicSettings?.key + config.mode = if (outbound?.streamSettings?.grpcSettings?.multiMode == true) "multi" else "gun" + config.serviceName = outbound?.streamSettings?.grpcSettings?.serviceName + config.authority = outbound?.streamSettings?.grpcSettings?.authority + + config.security = outbound.streamSettings?.security + val tlsSettings = outbound?.streamSettings?.realitySettings ?: outbound?.streamSettings?.tlsSettings + config.insecure = tlsSettings?.allowInsecure + config.sni = tlsSettings?.serverName + config.fingerPrint = tlsSettings?.fingerprint + config.alpn = Utils.removeWhiteSpace(tlsSettings?.alpn?.joinToString(",")).toString() + + config.publicKey = tlsSettings?.publicKey + config.shortId = tlsSettings?.shortId + config.spiderX = tlsSettings?.spiderX + + return config + } + + private fun migrate2ProfileSocks(configOld: ServerConfig): ProfileItem? { + val config = ProfileItem.create(EConfigType.SOCKS) + + val outbound = configOld.getProxyOutbound() ?: return null + config.remarks = configOld.remarks + config.server = outbound.getServerAddress() + config.serverPort = outbound.getServerPort().toString() + config.username = outbound.settings?.servers?.first()?.users?.first()?.user + config.password = outbound.getPassword() + + return config + } + + private fun migrate2ProfileHttp(configOld: ServerConfig): ProfileItem? { + val config = ProfileItem.create(EConfigType.HTTP) + + val outbound = configOld.getProxyOutbound() ?: return null + config.remarks = configOld.remarks + config.server = outbound.getServerAddress() + config.serverPort = outbound.getServerPort().toString() + config.username = outbound.settings?.servers?.first()?.users?.first()?.user + config.password = outbound.getPassword() + + return config + } + + private fun migrate2ProfileWireguard(configOld: ServerConfig): ProfileItem? { + val config = ProfileItem.create(EConfigType.WIREGUARD) + + val outbound = configOld.getProxyOutbound() ?: return null + config.remarks = configOld.remarks + config.server = outbound.getServerAddress() + config.serverPort = outbound.getServerPort().toString() + + outbound.settings?.let { wireguard -> + config.secretKey = wireguard.secretKey + config.localAddress = Utils.removeWhiteSpace((wireguard.address as List<*>).joinToString(",")).toString() + config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey + config.mtu = wireguard.mtu + config.reserved = Utils.removeWhiteSpace(wireguard.reserved?.joinToString(",")).toString() + } + return config + } + + private fun migrate2ProfileHysteria2(configOld: ServerConfig): ProfileItem? { + val config = ProfileItem.create(EConfigType.HYSTERIA2) + + val outbound = configOld.getProxyOutbound() ?: return null + config.remarks = configOld.remarks + config.server = outbound.getServerAddress() + config.serverPort = outbound.getServerPort().toString() + config.password = outbound.getPassword() + + config.security = AppConfig.TLS + outbound.streamSettings?.tlsSettings?.let { tlsSetting -> + config.insecure = tlsSetting.allowInsecure + config.sni = tlsSetting.serverName + config.alpn = Utils.removeWhiteSpace(tlsSetting.alpn?.joinToString(",")).orEmpty() + + } + config.obfsPassword = outbound.settings?.obfsPassword + + return config + } + + private fun migrate2ProfileCustom(configOld: ServerConfig): ProfileItem? { + val config = ProfileItem.create(EConfigType.CUSTOM) + + val outbound = configOld.getProxyOutbound() ?: return null + config.remarks = configOld.remarks + config.server = outbound.getServerAddress() + config.serverPort = outbound.getServerPort().toString() + + return config + } + + + private fun decodeServerConfigOld(guid: String): ServerConfig? { + if (guid.isBlank()) { + return null + } + val json = serverStorage.decodeString(guid) + if (json.isNullOrBlank()) { + return null + } + return JsonUtil.fromJson(json, ServerConfig::class.java) + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt new file mode 100644 index 0000000000..773699d01c --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt @@ -0,0 +1,370 @@ +package com.v2ray.ang.handler + + +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig.PREF_IS_BOOTED +import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET +import com.v2ray.ang.dto.AssetUrlItem +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.RulesetItem +import com.v2ray.ang.dto.ServerAffiliationInfo +import com.v2ray.ang.dto.SubscriptionItem +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.Utils + +object MmkvManager { + + //region private + + //private const val ID_PROFILE_CONFIG = "PROFILE_CONFIG" + private const val ID_MAIN = "MAIN" + private const val ID_PROFILE_FULL_CONFIG = "PROFILE_FULL_CONFIG" + private const val ID_SERVER_RAW = "SERVER_RAW" + private const val ID_SERVER_AFF = "SERVER_AFF" + private const val ID_SUB = "SUB" + private const val ID_ASSET = "ASSET" + private const val ID_SETTING = "SETTING" + private const val KEY_SELECTED_SERVER = "SELECTED_SERVER" + private const val KEY_ANG_CONFIGS = "ANG_CONFIGS" + private const val KEY_SUB_IDS = "SUB_IDS" + + //private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) } + private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) } + private val profileFullStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_FULL_CONFIG, MMKV.MULTI_PROCESS_MODE) } + private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) } + private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) } + private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) } + private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) } + private val settingsStorage by lazy { MMKV.mmkvWithID(ID_SETTING, MMKV.MULTI_PROCESS_MODE) } + + //endregion + + //region Server + + fun getSelectServer(): String? { + return mainStorage.decodeString(KEY_SELECTED_SERVER) + } + + fun setSelectServer(guid: String) { + mainStorage.encode(KEY_SELECTED_SERVER, guid) + } + + fun encodeServerList(serverList: MutableList) { + mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList)) + } + + fun decodeServerList(): MutableList { + val json = mainStorage.decodeString(KEY_ANG_CONFIGS) + return if (json.isNullOrBlank()) { + mutableListOf() + } else { + JsonUtil.fromJson(json, Array::class.java).toMutableList() + } + } + + + fun decodeServerConfig(guid: String): ProfileItem? { + if (guid.isBlank()) { + return null + } + val json = profileFullStorage.decodeString(guid) + if (json.isNullOrBlank()) { + return null + } + return JsonUtil.fromJson(json, ProfileItem::class.java) + } + +// fun decodeProfileConfig(guid: String): ProfileLiteItem? { +// if (guid.isBlank()) { +// return null +// } +// val json = profileStorage.decodeString(guid) +// if (json.isNullOrBlank()) { +// return null +// } +// return JsonUtil.fromJson(json, ProfileLiteItem::class.java) +// } + + fun encodeServerConfig(guid: String, config: ProfileItem): String { + val key = guid.ifBlank { Utils.getUuid() } + profileFullStorage.encode(key, JsonUtil.toJson(config)) + val serverList = decodeServerList() + if (!serverList.contains(key)) { + serverList.add(0, key) + encodeServerList(serverList) + if (getSelectServer().isNullOrBlank()) { + mainStorage.encode(KEY_SELECTED_SERVER, key) + } + } +// val profile = ProfileLiteItem( +// configType = config.configType, +// subscriptionId = config.subscriptionId, +// remarks = config.remarks, +// server = config.getProxyOutbound()?.getServerAddress(), +// serverPort = config.getProxyOutbound()?.getServerPort(), +// ) +// profileStorage.encode(key, JsonUtil.toJson(profile)) + return key + } + + fun removeServer(guid: String) { + if (guid.isBlank()) { + return + } + if (getSelectServer() == guid) { + mainStorage.remove(KEY_SELECTED_SERVER) + } + val serverList = decodeServerList() + serverList.remove(guid) + encodeServerList(serverList) + profileFullStorage.remove(guid) + //profileStorage.remove(guid) + serverAffStorage.remove(guid) + } + + fun removeServerViaSubid(subid: String) { + if (subid.isBlank()) { + return + } + profileFullStorage.allKeys()?.forEach { key -> + decodeServerConfig(key)?.let { config -> + if (config.subscriptionId == subid) { + removeServer(key) + } + } + } + } + + fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? { + if (guid.isBlank()) { + return null + } + val json = serverAffStorage.decodeString(guid) + if (json.isNullOrBlank()) { + return null + } + return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java) + } + + fun encodeServerTestDelayMillis(guid: String, testResult: Long) { + if (guid.isBlank()) { + return + } + val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo() + aff.testDelayMillis = testResult + serverAffStorage.encode(guid, JsonUtil.toJson(aff)) + } + + fun clearAllTestDelayResults(keys: List?) { + keys?.forEach { key -> + decodeServerAffiliationInfo(key)?.let { aff -> + aff.testDelayMillis = 0 + serverAffStorage.encode(key, JsonUtil.toJson(aff)) + } + } + } + + fun removeAllServer(): Int { + val count = profileFullStorage.allKeys()?.count() ?: 0 + mainStorage.clearAll() + profileFullStorage.clearAll() + //profileStorage.clearAll() + serverAffStorage.clearAll() + return count + } + + fun removeInvalidServer(guid: String): Int { + var count = 0 + if (guid.isNotEmpty()) { + decodeServerAffiliationInfo(guid)?.let { aff -> + if (aff.testDelayMillis < 0L) { + removeServer(guid) + count++ + } + } + } else { + serverAffStorage.allKeys()?.forEach { key -> + decodeServerAffiliationInfo(key)?.let { aff -> + if (aff.testDelayMillis < 0L) { + removeServer(key) + count++ + } + } + } + } + return count + } + + fun encodeServerRaw(guid: String, config: String) { + serverRawStorage.encode(guid, config) + } + + fun decodeServerRaw(guid: String): String? { + return serverRawStorage.decodeString(guid) + } + + //endregion + + //region Subscriptions + + private fun initSubsList() { + val subsList = decodeSubsList() + if (subsList.isNotEmpty()) { + return + } + subStorage.allKeys()?.forEach { key -> + subsList.add(key) + } + encodeSubsList(subsList) + } + + fun decodeSubscriptions(): List> { + initSubsList() + + val subscriptions = mutableListOf>() + decodeSubsList().forEach { key -> + val json = subStorage.decodeString(key) + if (!json.isNullOrBlank()) { + subscriptions.add(Pair(key, JsonUtil.fromJson(json, SubscriptionItem::class.java))) + } + } + return subscriptions + } + + fun removeSubscription(subid: String) { + subStorage.remove(subid) + val subsList = decodeSubsList() + subsList.remove(subid) + encodeSubsList(subsList) + + removeServerViaSubid(subid) + } + + fun encodeSubscription(guid: String, subItem: SubscriptionItem) { + val key = guid.ifBlank { Utils.getUuid() } + subStorage.encode(key, JsonUtil.toJson(subItem)) + + val subsList = decodeSubsList() + if (!subsList.contains(key)) { + subsList.add(key) + encodeSubsList(subsList) + } + } + + fun decodeSubscription(subscriptionId: String): SubscriptionItem? { + val json = subStorage.decodeString(subscriptionId) ?: return null + return JsonUtil.fromJson(json, SubscriptionItem::class.java) + } + + fun encodeSubsList(subsList: MutableList) { + mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList)) + } + + fun decodeSubsList(): MutableList { + val json = mainStorage.decodeString(KEY_SUB_IDS) + return if (json.isNullOrBlank()) { + mutableListOf() + } else { + JsonUtil.fromJson(json, Array::class.java).toMutableList() + } + } + + //endregion + + //region Asset + + fun decodeAssetUrls(): List> { + val assetUrlItems = mutableListOf>() + assetStorage.allKeys()?.forEach { key -> + val json = assetStorage.decodeString(key) + if (!json.isNullOrBlank()) { + assetUrlItems.add(Pair(key, JsonUtil.fromJson(json, AssetUrlItem::class.java))) + } + } + return assetUrlItems.sortedBy { (_, value) -> value.addedTime } + } + + fun removeAssetUrl(assetid: String) { + assetStorage.remove(assetid) + } + + fun encodeAsset(assetid: String, assetItem: AssetUrlItem) { + val key = assetid.ifBlank { Utils.getUuid() } + assetStorage.encode(key, JsonUtil.toJson(assetItem)) + } + + fun decodeAsset(assetid: String): AssetUrlItem? { + val json = assetStorage.decodeString(assetid) ?: return null + return JsonUtil.fromJson(json, AssetUrlItem::class.java) + } + + //endregion + + //region Routing + + fun decodeRoutingRulesets(): MutableList? { + val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET) + if (ruleset.isNullOrEmpty()) return null + return JsonUtil.fromJson(ruleset, Array::class.java).toMutableList() + } + + fun encodeRoutingRulesets(rulesetList: MutableList?) { + if (rulesetList.isNullOrEmpty()) + encodeSettings(PREF_ROUTING_RULESET, "") + else + encodeSettings(PREF_ROUTING_RULESET, JsonUtil.toJson(rulesetList)) + } + + //endregion + fun encodeSettings(key: String, value: String?): Boolean { + return settingsStorage.encode(key, value) + } + + fun encodeSettings(key: String, value: Int): Boolean { + return settingsStorage.encode(key, value) + } + + fun encodeSettings(key: String, value: Boolean): Boolean { + return settingsStorage.encode(key, value) + } + + fun encodeSettings(key: String, value: MutableSet): Boolean { + return settingsStorage.encode(key, value) + } + + + fun decodeSettingsString(key: String): String? { + return settingsStorage.decodeString(key) + } + + fun decodeSettingsString(key: String, defaultValue: String?): String? { + return settingsStorage.decodeString(key, defaultValue) + } + + fun decodeSettingsBool(key: String): Boolean { + return settingsStorage.decodeBool(key, false) + } + + fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean { + return settingsStorage.decodeBool(key, defaultValue) + } + + fun decodeSettingsStringSet(key: String): MutableSet? { + return settingsStorage.decodeStringSet(key) + } + + //endregion + + //region Others + + fun encodeStartOnBoot(startOnBoot: Boolean) { + MmkvManager.encodeSettings(PREF_IS_BOOTED, startOnBoot) + } + + fun decodeStartOnBoot(): Boolean { + return decodeSettingsBool(PREF_IS_BOOTED, false) + } + + //endregion + +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt new file mode 100644 index 0000000000..2e61c02f84 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt @@ -0,0 +1,210 @@ +package com.v2ray.ang.handler + +import android.content.Context +import android.content.res.AssetManager +import android.text.TextUtils +import android.util.Log +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.AppConfig.GEOIP_PRIVATE +import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE +import com.v2ray.ang.AppConfig.TAG_DIRECT +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.RoutingType +import com.v2ray.ang.dto.RulesetItem +import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.handler.MmkvManager.decodeServerConfig +import com.v2ray.ang.handler.MmkvManager.decodeServerList +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.Utils +import com.v2ray.ang.util.Utils.parseInt +import java.io.File +import java.io.FileOutputStream +import java.util.Collections +import kotlin.Int + +object SettingsManager { + + fun initRoutingRulesets(context: Context) { + val exist = MmkvManager.decodeRoutingRulesets() + if (exist.isNullOrEmpty()) { + // set iran lite routing rules from index 3 + val rulesetList = getPresetRoutingRulesets(context,3) + MmkvManager.encodeRoutingRulesets(rulesetList) + } + } + + private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList? { + val fileName = RoutingType.fromIndex(index).fileName + val assets = Utils.readTextFromAssets(context, fileName) + if (TextUtils.isEmpty(assets)) { + return null + } + + return JsonUtil.fromJson(assets, Array::class.java).toMutableList() + } + + + fun resetRoutingRulesetsFromPresets(context: Context, index: Int) { + val rulesetList = getPresetRoutingRulesets(context, index) ?: return + resetRoutingRulesetsCommon(rulesetList) + } + + fun resetRoutingRulesets(content: String?): Boolean { + if (content.isNullOrEmpty()) { + return false + } + + try { + val rulesetList = JsonUtil.fromJson(content, Array::class.java).toMutableList() + if (rulesetList.isNullOrEmpty()) { + return false + } + + resetRoutingRulesetsCommon(rulesetList) + return true + } catch (e: Exception) { + e.printStackTrace() + return false + } + } + + private fun resetRoutingRulesetsCommon(rulesetList: MutableList) { + val rulesetNew: MutableList = mutableListOf() + MmkvManager.decodeRoutingRulesets()?.forEach { key -> + if (key.locked == true) { + rulesetNew.add(key) + } + } + + rulesetNew.addAll(rulesetList) + MmkvManager.encodeRoutingRulesets(rulesetNew) + } + + fun getRoutingRuleset(index: Int): RulesetItem? { + if (index < 0) return null + + val rulesetList = MmkvManager.decodeRoutingRulesets() + if (rulesetList.isNullOrEmpty()) return null + + return rulesetList[index] + } + + fun saveRoutingRuleset(index: Int, ruleset: RulesetItem?) { + if (ruleset == null) return + + var rulesetList = MmkvManager.decodeRoutingRulesets() + if (rulesetList.isNullOrEmpty()) { + rulesetList = mutableListOf() + } + + if (index < 0 || index >= rulesetList.count()) { + rulesetList.add(0, ruleset) + } else { + rulesetList[index] = ruleset + } + MmkvManager.encodeRoutingRulesets(rulesetList) + } + + fun removeRoutingRuleset(index: Int) { + if (index < 0) return + + val rulesetList = MmkvManager.decodeRoutingRulesets() + if (rulesetList.isNullOrEmpty()) return + + rulesetList.removeAt(index) + MmkvManager.encodeRoutingRulesets(rulesetList) + } + + fun routingRulesetsBypassLan(): Boolean { + val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "0" + if (vpnBypassLan == "1") { + return true + } else if (vpnBypassLan == "2") { + return false + } + + //Follow config + val guid = MmkvManager.getSelectServer() ?: return false + val config = MmkvManager.decodeServerConfig(guid) ?: return false + if (config.configType == EConfigType.CUSTOM) { + val raw = MmkvManager.decodeServerRaw(guid) ?: return false + val v2rayConfig = JsonUtil.fromJson(raw, V2rayConfig::class.java) + val exist = v2rayConfig.routing.rules.filter { it.outboundTag == TAG_DIRECT }?.any { + it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true + } + return exist == true + } + + val rulesetItems = MmkvManager.decodeRoutingRulesets() + val exist = rulesetItems?.filter { it.enabled && it.outboundTag == TAG_DIRECT }?.any { + it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true + } + return exist == true + } + + fun swapRoutingRuleset(fromPosition: Int, toPosition: Int) { + val rulesetList = MmkvManager.decodeRoutingRulesets() + if (rulesetList.isNullOrEmpty()) return + + Collections.swap(rulesetList, fromPosition, toPosition) + MmkvManager.encodeRoutingRulesets(rulesetList) + } + + fun swapSubscriptions(fromPosition: Int, toPosition: Int) { + val subsList = MmkvManager.decodeSubsList() + if (subsList.isNullOrEmpty()) return + + Collections.swap(subsList, fromPosition, toPosition) + MmkvManager.encodeSubsList(subsList) + } + + fun getServerViaRemarks(remarks: String?): ProfileItem? { + if (remarks == null) { + return null + } + val serverList = decodeServerList() + for (guid in serverList) { + val profile = decodeServerConfig(guid) + if (profile != null && profile.remarks == remarks) { + return profile + } + } + return null + } + + fun getSocksPort(): Int { + return parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt()) + } + + fun getHttpPort(): Int { + return getSocksPort() + (if (Utils.isXray()) 0 else 1) + } + + fun initAssets(context: Context, assets: AssetManager) { + val extFolder = Utils.userAssetPath(context) + + try { + val geo = arrayOf("geosite.dat", "geoip.dat", "geosite_c4u.dat", "geoip_c4u.dat") + assets.list("") + ?.filter { geo.contains(it) } + ?.filter { !File(extFolder, it).exists() } + ?.forEach { + val target = File(extFolder, it) + assets.open(it).use { input -> + FileOutputStream(target).use { output -> + input.copyTo(output) + } + } + Log.i( + ANG_PACKAGE, + "Copied from apk assets folder to ${target.absolutePath}" + ) + } + } catch (e: Exception) { + Log.e(ANG_PACKAGE, "asset copy failed", e) + } + + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt new file mode 100644 index 0000000000..53d9e326ae --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt @@ -0,0 +1,651 @@ +package com.v2ray.ang.handler + +import android.content.Context +import android.text.TextUtils +import android.util.Log +import com.v2ray.ang.AppConfig +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_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 +import com.v2ray.ang.AppConfig.GOOGLEAPIS_CN_DOMAIN +import com.v2ray.ang.AppConfig.GOOGLEAPIS_COM_DOMAIN +import com.v2ray.ang.AppConfig.HEADER_TYPE_HTTP +import com.v2ray.ang.AppConfig.LOOPBACK +import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM +import com.v2ray.ang.AppConfig.TAG_BLOCKED +import com.v2ray.ang.AppConfig.TAG_DIRECT +import com.v2ray.ang.AppConfig.TAG_FRAGMENT +import com.v2ray.ang.AppConfig.TAG_PROXY +import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 +import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6 +import com.v2ray.ang.dto.ConfigResult +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.NetworkType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.RulesetItem +import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean +import com.v2ray.ang.extension.isNotNullEmpty +import com.v2ray.ang.fmt.HttpFmt +import com.v2ray.ang.fmt.Hysteria2Fmt +import com.v2ray.ang.fmt.ShadowsocksFmt +import com.v2ray.ang.fmt.SocksFmt +import com.v2ray.ang.fmt.TrojanFmt +import com.v2ray.ang.fmt.VlessFmt +import com.v2ray.ang.fmt.VmessFmt +import com.v2ray.ang.fmt.WireguardFmt +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.Utils + +object V2rayConfigManager { + + fun getV2rayConfig(context: Context, guid: String): ConfigResult { + try { + val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false) + if (config.configType == EConfigType.CUSTOM) { + val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false) + val domainPort = config.getServerAddressAndPort() + return ConfigResult(true, guid, raw, domainPort) + } + + val result = getV2rayNonCustomConfig(context, config) + //Log.d(ANG_PACKAGE, result.content) + result.guid = guid + return result + } catch (e: Exception) { + e.printStackTrace() + return ConfigResult(false) + } + } + + private fun getV2rayNonCustomConfig(context: Context, config: ProfileItem): ConfigResult { + val result = ConfigResult(false) + + val address = config.server ?: return result + if (!Utils.isIpAddress(address)) { + if (!Utils.isValidUrl(address)) { + Log.d(ANG_PACKAGE, "$address is an invalid ip or domain") + return result + } + } + + //取得默认配置 + val assets = Utils.readTextFromAssets(context, "v2ray_config.json") + if (TextUtils.isEmpty(assets)) { + return result + } + val v2rayConfig = JsonUtil.fromJson(assets, V2rayConfig::class.java) ?: return result + v2rayConfig.log.loglevel = + MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning" + v2rayConfig.remarks = config.remarks + + inbounds(v2rayConfig) + + val isPlugin = config.configType == EConfigType.HYSTERIA2 + val retOut = outbounds(v2rayConfig, config, isPlugin) ?: return result + val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin) + + routing(v2rayConfig) + + fakedns(v2rayConfig) + + dns(v2rayConfig) + + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { + customLocalDns(v2rayConfig) + } + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) { + v2rayConfig.stats = null + v2rayConfig.policy = null + } + + result.status = true + result.content = v2rayConfig.toPrettyPrinting() + result.domainPort = if (retMore.first) retMore.second else retOut.second + return result + } + + private fun inbounds(v2rayConfig: V2rayConfig): Boolean { + try { + val socksPort = SettingsManager.getSocksPort() + + v2rayConfig.inbounds.forEach { curInbound -> + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) { + //bind all inbounds to localhost if the user requests + curInbound.listen = LOOPBACK + } + } + v2rayConfig.inbounds[0].port = socksPort + val fakedns = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true + val sniffAllTlsAndHttp = + MmkvManager.decodeSettingsBool(AppConfig.PREF_SNIFFING_ENABLED, true) != false + v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp + v2rayConfig.inbounds[0].sniffing?.routeOnly = + MmkvManager.decodeSettingsBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false) + if (!sniffAllTlsAndHttp) { + v2rayConfig.inbounds[0].sniffing?.destOverride?.clear() + } + if (fakedns) { + v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns") + } + + if (Utils.isXray()) { + v2rayConfig.inbounds.removeAt(1) + } else { + val httpPort = SettingsManager.getHttpPort() + v2rayConfig.inbounds[1].port = httpPort + } + + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + private fun outbounds(v2rayConfig: V2rayConfig, config: ProfileItem, isPlugin: Boolean): Pair? { + if (isPlugin) { + val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0)) + val outboundNew = V2rayConfig.OutboundBean( + mux = null, + protocol = EConfigType.SOCKS.name.lowercase(), + settings = V2rayConfig.OutboundBean.OutSettingsBean( + servers = listOf( + V2rayConfig.OutboundBean.OutSettingsBean.ServersBean( + address = LOOPBACK, + port = socksPort + ) + ) + ) + ) + if (v2rayConfig.outbounds.isNotEmpty()) { + v2rayConfig.outbounds[0] = outboundNew + } else { + v2rayConfig.outbounds.add(outboundNew) + } + return Pair(true, outboundNew.getServerAddressAndPort()) + } + + val outbound = getProxyOutbound(config) ?: return null + val ret = updateOutboundWithGlobalSettings(outbound) + if (!ret) return null + + if (v2rayConfig.outbounds.isNotEmpty()) { + v2rayConfig.outbounds[0] = outbound + } else { + v2rayConfig.outbounds.add(outbound) + } + + updateOutboundFragment(v2rayConfig) + return Pair(true, config.getServerAddressAndPort()) + } + + private fun fakedns(v2rayConfig: V2rayConfig) { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true + && MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true + ) { + v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean()) + } + } + + private fun routing(v2rayConfig: V2rayConfig): Boolean { + try { + + v2rayConfig.routing.domainStrategy = + MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) + ?: "IPIfNonMatch" + + val rulesetItems = MmkvManager.decodeRoutingRulesets() + rulesetItems?.forEach { key -> + routingUserRule(key, v2rayConfig) + } + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + private fun routingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) { + try { + if (item == null || !item.enabled) { + return + } + + val rule = JsonUtil.fromJson(JsonUtil.toJson(item), RulesBean::class.java) ?: return + + v2rayConfig.routing.rules.add(rule) + + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun userRule2Domain(tag: String): ArrayList { + val domain = ArrayList() + + val rulesetItems = MmkvManager.decodeRoutingRulesets() + rulesetItems?.forEach { key -> + if (key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) { + key.domain?.forEach { + if (it != GEOSITE_PRIVATE + && ( it.startsWith("geosite:") || it.startsWith("domain:") || it.startsWith("ext:geosite") || it.startsWith("regexp:") ) + ) { + domain.add(it) + } + } + } + } + + return domain + } + + private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean { + try { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) { + val geositeCn = arrayListOf(GEOSITE_CN) + val proxyDomain = userRule2Domain(TAG_PROXY) + val directDomain = userRule2Domain(TAG_DIRECT) + // fakedns with all domains to make it always top priority + v2rayConfig.dns.servers?.add( + 0, + V2rayConfig.DnsBean.ServersBean( + address = "fakedns", + domains = geositeCn.plus(proxyDomain).plus(directDomain) + ) + ) + } + + // DNS inbound对象 + val remoteDns = Utils.getRemoteDnsServers() + if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) { + val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean( + address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else AppConfig.DNS_PROXY, + port = 53, + network = "tcp,udp" + ) + + val localDnsPort = Utils.parseInt( + MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), + AppConfig.PORT_LOCAL_DNS.toInt() + ) + v2rayConfig.inbounds.add( + V2rayConfig.InboundBean( + tag = "dns-in", + port = localDnsPort, + listen = LOOPBACK, + protocol = "dokodemo-door", + settings = dnsInboundSettings, + sniffing = null + ) + ) + } + + // DNS outbound对象 + if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) { + v2rayConfig.outbounds.add( + V2rayConfig.OutboundBean( + protocol = "dns", + tag = "dns-out", + settings = null, + streamSettings = null, + mux = null + ) + ) + } + + // DNS routing tag + v2rayConfig.routing.rules.add( + 0, RulesBean( + inboundTag = arrayListOf("dns-in"), + outboundTag = "dns-out", + domain = null + ) + ) + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + private fun dns(v2rayConfig: V2rayConfig): Boolean { + try { + val hosts = mutableMapOf() + val servers = ArrayList() + + //remote Dns + val remoteDns = Utils.getRemoteDnsServers() + val proxyDomain = userRule2Domain(TAG_PROXY) + remoteDns.forEach { + servers.add(it) + } + if (proxyDomain.isNotEmpty()) { + servers.add( + V2rayConfig.DnsBean.ServersBean( + address = remoteDns.first(), + domains = proxyDomain, + ) + ) + } + + // domestic DNS + val domesticDns = Utils.getDomesticDnsServers() + val directDomain = userRule2Domain(TAG_DIRECT) + val isCnRoutingMode = directDomain.contains(GEOSITE_CN) + val geoipCn = arrayListOf(GEOIP_CN) + if (directDomain.isNotEmpty()) { + servers.add( + V2rayConfig.DnsBean.ServersBean( + address = domesticDns.first(), + domains = directDomain, + expectIPs = if (isCnRoutingMode) geoipCn else null, + skipFallback = true + ) + ) + } + + if (Utils.isPureIpAddress(domesticDns.first())) { + v2rayConfig.routing.rules.add( + 0, RulesBean( + outboundTag = TAG_DIRECT, + port = "53", + ip = arrayListOf(domesticDns.first()), + domain = null + ) + ) + } + + //User DNS hosts + try { + val userHosts = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS) + if (userHosts.isNotNullEmpty()) { + var userHostsMap = userHosts?.split(",") + ?.filter { it.isNotEmpty() } + ?.filter { it.contains(":") } + ?.associate { it.split(":").let { (k, v) -> k to v } } + if (userHostsMap != null) hosts.putAll(userHostsMap) + } + } catch (e: Exception) { + e.printStackTrace() + } + + //block dns + val blkDomain = userRule2Domain(TAG_BLOCKED) + if (blkDomain.isNotEmpty()) { + hosts.putAll(blkDomain.map { it to LOOPBACK }) + } + + // hardcode googleapi rule to fix play store problems + hosts[GOOGLEAPIS_CN_DOMAIN] = GOOGLEAPIS_COM_DOMAIN + + // hardcode popular Android Private DNS rule to fix localhost DNS problem + hosts[DNS_ALIDNS_DOMAIN] = DNS_ALIDNS_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对象 + v2rayConfig.dns = V2rayConfig.DnsBean( + servers = servers, + hosts = hosts + ) + + // DNS routing + if (Utils.isPureIpAddress(remoteDns.first())) { + v2rayConfig.routing.rules.add( + 0, RulesBean( + outboundTag = TAG_PROXY, + port = "53", + ip = arrayListOf(remoteDns.first()), + domain = null + ) + ) + } + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean { + try { + var muxEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false) + val protocol = outbound.protocol + 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.WIREGUARD.name, true) + || protocol.equals(EConfigType.HYSTERIA2.name, true) + ) { + muxEnabled = false + } else if (outbound.streamSettings?.network == NetworkType.XHTTP.type) { + muxEnabled = false + } + + if (muxEnabled == true) { + outbound.mux?.enabled = true + outbound.mux?.concurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8").orEmpty().toInt() + outbound.mux?.xudpConcurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "16").orEmpty().toInt() + outbound.mux?.xudpProxyUDP443 = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC,"reject") + if (protocol.equals(EConfigType.VLESS.name, true) && outbound.settings?.vnext?.first()?.users?.first()?.flow?.isNotEmpty() == true) { + outbound.mux?.concurrency = -1 + } + } else { + outbound.mux?.enabled = false + outbound.mux?.concurrency = -1 + } + + if (protocol.equals(EConfigType.WIREGUARD.name, true)) { + var localTunAddr = if (outbound.settings?.address == null) { + listOf(WIREGUARD_LOCAL_ADDRESS_V4, WIREGUARD_LOCAL_ADDRESS_V6) + } else { + outbound.settings?.address as List<*> + } + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) != true) { + localTunAddr = listOf(localTunAddr.first()) + } + outbound.settings?.address = localTunAddr + } + + if (outbound.streamSettings?.network == DEFAULT_NETWORK + && outbound.streamSettings?.tcpSettings?.header?.type == HEADER_TYPE_HTTP + ) { + val path = outbound.streamSettings?.tcpSettings?.header?.request?.path + 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"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}""" + } + outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson( + requestString, + V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java + ) + outbound.streamSettings?.tcpSettings?.header?.request?.path = + if (path.isNullOrEmpty()) { + listOf("/") + } else { + path + } + outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host + } + + + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean { + try { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) { + return true + } + + if (v2rayConfig.outbounds[0].protocol.lowercase() == "wireguard") { + return true + } + + if (v2rayConfig.outbounds[0].streamSettings?.network == "quic") { + return true + } + + val fragmentOutbound = + V2rayConfig.OutboundBean( + protocol = PROTOCOL_FREEDOM, + tag = TAG_FRAGMENT, + mux = null + ) + + var packets = + MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello" + if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.REALITY + && packets == "tlshello" + ) { + packets = "1-3" + } else if (v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.TLS + && packets == "tlshello" + ) { + packets = "fakehost" + } + + fragmentOutbound.settings = V2rayConfig.OutboundBean.OutSettingsBean( + fragment = V2rayConfig.OutboundBean.OutSettingsBean.FragmentBean( + packets = packets, + length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH) + ?: "10-20", + interval = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL) + ?: "10-20" + ), + noises = listOf( + V2rayConfig.OutboundBean.OutSettingsBean.NoiseBean( + type = "rand", + packet = "10-20", + delay = "10-16", + ) + ), + ) + fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean( + sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean( + TcpNoDelay = true, + mark = 255 + ) + ) + v2rayConfig.outbounds.add(fragmentOutbound) + + //proxy chain + v2rayConfig.outbounds[0].streamSettings?.sockopt = + V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean( + dialerProxy = TAG_FRAGMENT + ) + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + private fun moreOutbounds( + v2rayConfig: V2rayConfig, + subscriptionId: String, + isPlugin: Boolean + ): Pair { + val returnPair = Pair(false, "") + var domainPort: String = "" + + if (isPlugin) { + return returnPair + } + //fragment proxy + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) { + return returnPair + } + + if (subscriptionId.isEmpty()) { + return returnPair + } + try { + val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return returnPair + + //current proxy + val outbound = v2rayConfig.outbounds[0] + + //Previous proxy + val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile) + if (prevNode != null) { + val prevOutbound = getProxyOutbound(prevNode) + if (prevOutbound != null) { + updateOutboundWithGlobalSettings(prevOutbound) + prevOutbound.tag = TAG_PROXY + "2" + v2rayConfig.outbounds.add(prevOutbound) + outbound.streamSettings?.sockopt = + V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean( + dialerProxy = prevOutbound.tag + ) + domainPort = prevNode.getServerAddressAndPort() + } + } + + //Next proxy + val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile) + if (nextNode != null) { + val nextOutbound = getProxyOutbound(nextNode) + if (nextOutbound != null) { + updateOutboundWithGlobalSettings(nextOutbound) + nextOutbound.tag = TAG_PROXY + v2rayConfig.outbounds.add(0, nextOutbound) + outbound.tag = TAG_PROXY + "1" + nextOutbound.streamSettings?.sockopt = + V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean( + dialerProxy = outbound.tag + ) + } + } + } catch (e: Exception) { + e.printStackTrace() + return returnPair + } + + if (domainPort.isNotEmpty()) { + return Pair(true, domainPort) + } + return returnPair + } + + fun getProxyOutbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? { + return when (profileItem.configType) { + EConfigType.VMESS -> VmessFmt.toOutbound(profileItem) + EConfigType.CUSTOM -> null + EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toOutbound(profileItem) + EConfigType.SOCKS -> SocksFmt.toOutbound(profileItem) + EConfigType.VLESS -> VlessFmt.toOutbound(profileItem) + EConfigType.TROJAN -> TrojanFmt.toOutbound(profileItem) + EConfigType.WIREGUARD -> WireguardFmt.toOutbound(profileItem) + EConfigType.HYSTERIA2 -> Hysteria2Fmt.toOutbound(profileItem) + EConfigType.HTTP -> HttpFmt.toOutbound(profileItem) + } + + } + +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.kt similarity index 53% rename from V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java rename to V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.kt index 566feffcce..8707d16fa3 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.kt @@ -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 not at the end of a "drop" event.
- *
- * Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after + * an item is shifted, and **not** at the end of a "drop" event.

+ *

+ * 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.
- *
- * Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after + * Called when an item has been dismissed by a swipe.

+ *

+ * 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) } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.kt similarity index 67% rename from V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java rename to V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.kt index 149768fc1e..75655b0c85 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.kt @@ -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() } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java deleted file mode 100644 index a6407b6f5f..0000000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java +++ /dev/null @@ -1,33 +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 androidx.recyclerview.widget.RecyclerView; - -/** - * Listener for manual initiation of a drag. - */ -public interface OnStartDragListener { - - /** - * Called when a view is requesting a start of a drag. - * - * @param viewHolder The holder of the view to drag. - */ - void onStartDrag(RecyclerView.ViewHolder viewHolder); - -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java deleted file mode 100644 index e3686921b8..0000000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java +++ /dev/null @@ -1,129 +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.graphics.Canvas; - -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.ItemTouchHelper; -import androidx.recyclerview.widget.RecyclerView; - -import org.jetbrains.annotations.NotNull; - -/** - * 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.
- *
- * Expects the RecyclerView.Adapter to listen for {@link - * ItemTouchHelperAdapter} callbacks and the RecyclerView.ViewHolder to implement - * {@link ItemTouchHelperViewHolder}. - * - * @author Paul Burke (ipaulpro) - */ -public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback { - - public static final float ALPHA_FULL = 1.0f; - - private final ItemTouchHelperAdapter mAdapter; - - public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) { - mAdapter = adapter; - } - - @Override - public boolean isLongPressDragEnabled() { - return true; - } - - @Override - public boolean isItemViewSwipeEnabled() { - return false; - } - - @Override - public int getMovementFlags(RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) { - // Set movement flags based on the layout manager - if (recyclerView.getLayoutManager() instanceof GridLayoutManager) { - final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; - final int swipeFlags = 0; - 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(@NotNull RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { - if (source.getItemViewType() != target.getItemViewType()) { - return false; - } - - // Notify the adapter of the move - mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition()); - return true; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) { - // Notify the adapter of the dismissal - mAdapter.onItemDismiss(viewHolder.getBindingAdapterPosition()); - } - - @Override - public void onChildDraw(@NotNull Canvas c, @NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder, float dX, - float dY, int actionState, boolean isCurrentlyActive) { - if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { - // Fade out the view as it is swiped out of the parent's bounds - final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth(); - viewHolder.itemView.setAlpha(alpha); - viewHolder.itemView.setTranslationX(dX); - } else { - super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); - } - } - - @Override - public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { - // We only want the active item to change - if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { - if (viewHolder instanceof ItemTouchHelperViewHolder) { - // Let the view holder know that this item is being moved or dragged - ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder; - itemViewHolder.onItemSelected(); - } - } - - super.onSelectedChanged(viewHolder, actionState); - } - - @Override - public void clearView(@NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) { - super.clearView(recyclerView, viewHolder); - - mAdapter.onItemMoveCompleted(); - - viewHolder.itemView.setAlpha(ALPHA_FULL); - - if (viewHolder instanceof ItemTouchHelperViewHolder) { - // Tell the view holder it's time to restore the idle state - ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder; - itemViewHolder.onItemClear(); - } - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt new file mode 100644 index 0000000000..ff61aadc38 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt @@ -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.

+ * + * 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 + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/NativePlugin.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/NativePlugin.kt new file mode 100644 index 0000000000..7a072cd0f1 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/NativePlugin.kt @@ -0,0 +1,32 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + ******************************************************************************/ + +package com.v2ray.ang.plugin + +import android.content.pm.ResolveInfo + +class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) { + init { + check(resolveInfo.providerInfo != null) + } + + override val componentInfo get() = resolveInfo.providerInfo!! +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/Plugin.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/Plugin.kt new file mode 100644 index 0000000000..04294ac61a --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/Plugin.kt @@ -0,0 +1,43 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + ******************************************************************************/ + +package com.v2ray.ang.plugin + +import android.graphics.drawable.Drawable + +abstract class Plugin { + abstract val id: String + abstract val label: CharSequence + abstract val version: Int + abstract val versionName: String + open val icon: Drawable? get() = null + open val defaultConfig: String? get() = null + open val packageName: String get() = "" + open val directBootAware: Boolean get() = true + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return id == (other as Plugin).id + } + + override fun hashCode() = id.hashCode() +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginContract.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginContract.kt new file mode 100644 index 0000000000..5aa253f828 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginContract.kt @@ -0,0 +1,33 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + ******************************************************************************/ + +package com.v2ray.ang.plugin + +object PluginContract { + + const val ACTION_NATIVE_PLUGIN = "io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN" + const val EXTRA_ENTRY = "io.nekohasekai.sagernet.plugin.EXTRA_ENTRY" + const val METADATA_KEY_ID = "io.nekohasekai.sagernet.plugin.id" + const val METADATA_KEY_EXECUTABLE_PATH = "io.nekohasekai.sagernet.plugin.executable_path" + const val METHOD_GET_EXECUTABLE = "sagernet:getExecutable" + + const val COLUMN_PATH = "path" + const val COLUMN_MODE = "mode" + const val SCHEME = "plugin" +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginList.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginList.kt new file mode 100644 index 0000000000..2495eb4584 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginList.kt @@ -0,0 +1,54 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + ******************************************************************************/ + +package com.v2ray.ang.plugin + +import android.content.Intent +import android.content.pm.PackageManager +import com.v2ray.ang.AngApplication + +class PluginList : ArrayList() { + init { + addAll( + AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA + ) + .filter { it.providerInfo.exported }.map { NativePlugin(it) }) + } + + val lookup = mutableMapOf().apply { + for (plugin in this@PluginList.toList()) { + fun check(old: Plugin?) { + if (old != null && old != plugin) { + this@PluginList.remove(old) + } + /* if (old != null && old !== plugin) { + val packages = this@PluginList.filter { it.id == plugin.id } + .joinToString { it.packageName } + val message = "Conflicting plugins found from: $packages" + Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show() + throw IllegalStateException(message) + }*/ + } + check(put(plugin.id, plugin)) + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginManager.kt new file mode 100644 index 0000000000..121cfd33d3 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginManager.kt @@ -0,0 +1,233 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + ******************************************************************************/ + +package com.v2ray.ang.plugin + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.ContentResolver +import android.content.Intent +import android.content.pm.ComponentInfo +import android.content.pm.PackageManager +import android.content.pm.ProviderInfo +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.system.Os +import android.widget.Toast +import androidx.core.os.bundleOf +import com.v2ray.ang.AngApplication +import com.v2ray.ang.extension.listenForPackageChanges +import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID +import java.io.File +import java.io.FileNotFoundException + +object PluginManager { + + class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin) + + private var receiver: BroadcastReceiver? = null + private var cachedPlugins: PluginList? = null + fun fetchPlugins() = synchronized(this) { + if (receiver == null) receiver = AngApplication.application.listenForPackageChanges { + synchronized(this) { + receiver = null + cachedPlugins = null + } + } + if (cachedPlugins == null) cachedPlugins = PluginList() + cachedPlugins!! + } + + private fun buildUri(id: String, authority: String) = Uri.Builder() + .scheme(PluginContract.SCHEME) + .authority(authority) + .path("/$id") + .build() + + data class InitResult( + val path: String, + ) + + @Throws(Throwable::class) + fun init(pluginId: String): InitResult? { + if (pluginId.isEmpty()) return null + var throwable: Throwable? = null + + try { + val result = initNative(pluginId) + if (result != null) return result + } catch (t: Throwable) { + if (throwable == null) throwable = t //Logs.w(t) + } + + throw throwable ?: PluginNotFoundException(pluginId) + } + + private fun initNative(pluginId: String): InitResult? { + var flags = PackageManager.GET_META_DATA + if (Build.VERSION.SDK_INT >= 24) { + flags = + flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE + } + var providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "com.github.dyhkwong.AngApplication")), flags + ) + .filter { it.providerInfo.exported } + if (providers.isEmpty()) { + providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "io.nekohasekai.AngApplication")), flags + ) + .filter { it.providerInfo.exported } + } + if (providers.isEmpty()) { + providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "moe.matsuri.lite")), flags + ) + .filter { it.providerInfo.exported } + } + if (providers.isEmpty()) { + providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "fr.husi")), flags + ) + .filter { it.providerInfo.exported } + } + if (providers.isEmpty()) { + providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA + ).filter { + it.providerInfo.exported && + it.providerInfo.metaData.containsKey(METADATA_KEY_ID) && + it.providerInfo.metaData.getString(METADATA_KEY_ID) == pluginId + } + if (providers.size > 1) { + providers = listOf(providers[0]) // What if there is more than one? + } + } + if (providers.isEmpty()) return null + if (providers.size > 1) { + val message = + "Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}" + Toast.makeText(AngApplication.application, message, Toast.LENGTH_LONG).show() + throw IllegalStateException(message) + } + val provider = providers.single().providerInfo + var failure: Throwable? = null + try { + initNativeFaster(provider)?.also { return InitResult(it) } + } catch (t: Throwable) { + // Logs.w("Initializing native plugin faster mode failed") + failure = t + } + + val uri = Uri.Builder().apply { + scheme(ContentResolver.SCHEME_CONTENT) + authority(provider.authority) + }.build() + try { + return initNativeFast( + AngApplication.application.contentResolver, + pluginId, + uri + )?.let { InitResult(it) } + } catch (t: Throwable) { + // Logs.w("Initializing native plugin fast mode failed") + failure?.also { t.addSuppressed(it) } + failure = t + } + + try { + return initNativeSlow( + AngApplication.application.contentResolver, + pluginId, + uri + )?.let { InitResult(it) } + } catch (t: Throwable) { + failure?.also { t.addSuppressed(it) } + throw t + } + } + + private fun initNativeFaster(provider: ProviderInfo): String? { + return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH) + ?.let { relativePath -> + File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply { + check(canExecute()) + }.absolutePath + } + } + + private fun initNativeFast(cr: ContentResolver, pluginId: String, uri: Uri): String? { + return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, bundleOf()) + ?.getString(PluginContract.EXTRA_ENTRY)?.also { + check(File(it).canExecute()) + } + } + + @SuppressLint("Recycle") + private fun initNativeSlow(cr: ContentResolver, pluginId: String, uri: Uri): String? { + var initialized = false + fun entryNotFound(): Nothing = + throw IndexOutOfBoundsException("Plugin entry binary not found") + + val pluginDir = File(AngApplication.application.noBackupFilesDir, "plugin") + (cr.query( + uri, + arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), + null, + null, + null + ) + ?: return null).use { cursor -> + if (!cursor.moveToFirst()) entryNotFound() + pluginDir.deleteRecursively() + if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory") + val pluginDirPath = pluginDir.absolutePath + '/' + do { + val path = cursor.getString(0) + val file = File(pluginDir, path) + check(file.absolutePath.startsWith(pluginDirPath)) + cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream -> + file.outputStream().use { outStream -> inStream.copyTo(outStream) } + } + Os.chmod( + file.absolutePath, when (cursor.getType(1)) { + Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1) + Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8) + else -> throw IllegalArgumentException("File mode should be of type int") + } + ) + if (path == pluginId) initialized = true + } while (cursor.moveToNext()) + } + if (!initialized) entryNotFound() + return File(pluginDir, pluginId).absolutePath + } + + fun ComponentInfo.loadString(key: String) = when (val value = metaData.getString(key)) { + is String -> value + is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo) + .getString(value) + + null -> null + else -> error("meta-data $key has invalid type ${value.javaClass}") + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/ResolvedPlugin.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/ResolvedPlugin.kt new file mode 100644 index 0000000000..6e6861cf36 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/ResolvedPlugin.kt @@ -0,0 +1,51 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + ******************************************************************************/ + +package com.v2ray.ang.plugin + +import android.content.pm.ComponentInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.graphics.drawable.Drawable +import android.os.Build +import com.v2ray.ang.AngApplication +import com.v2ray.ang.plugin.PluginManager.loadString + +abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() { + protected abstract val componentInfo: ComponentInfo + + override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! } + override val version by lazy { + getPackageInfo(componentInfo.packageName).versionCode + } + override val versionName: String by lazy { + getPackageInfo(componentInfo.packageName).versionName!! + } + override val label: CharSequence get() = resolveInfo.loadLabel(AngApplication.application.packageManager) + override val icon: Drawable get() = resolveInfo.loadIcon(AngApplication.application.packageManager) + override val packageName: String get() = componentInfo.packageName + override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware + + fun getPackageInfo(packageName: String) = AngApplication.application.packageManager.getPackageInfo( + packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES + else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES + )!! +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt new file mode 100644 index 0000000000..acc8673097 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt @@ -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) + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/TaskerReceiver.kt similarity index 75% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/receiver/TaskerReceiver.kt index bf522425f3..c6e49af471 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/TaskerReceiver.kt @@ -4,30 +4,27 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.text.TextUtils -import com.tencent.mmkv.MMKV import com.v2ray.ang.AppConfig +import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.service.V2RayServiceManager -import com.v2ray.ang.util.MmkvManager - import com.v2ray.ang.util.Utils class TaskerReceiver : BroadcastReceiver() { - private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) } override fun onReceive(context: Context, intent: Intent?) { 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) { Utils.startVServiceFromToggle(context) } else { - mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid) + MmkvManager.setSelectServer(guid) V2RayServiceManager.startV2Ray(context) } } else { diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/WidgetProvider.kt similarity index 100% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/receiver/WidgetProvider.kt diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/ProcessService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/ProcessService.kt new file mode 100644 index 0000000000..89ab476f37 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/ProcessService.kt @@ -0,0 +1,44 @@ +package com.v2ray.ang.service + +import android.content.Context +import android.util.Log +import com.v2ray.ang.AppConfig.ANG_PACKAGE +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ProcessService { + private var process: Process? = null + + fun runProcess(context: Context, cmd: MutableList) { + Log.d(ANG_PACKAGE, cmd.toString()) + + try { + val proBuilder = ProcessBuilder(cmd) + proBuilder.redirectErrorStream(true) + process = proBuilder + .directory(context.filesDir) + .start() + + CoroutineScope(Dispatchers.IO).launch { + Thread.sleep(50L) + Log.d(ANG_PACKAGE, "runProcess check") + process?.waitFor() + Log.d(ANG_PACKAGE, "runProcess exited") + } + Log.d(ANG_PACKAGE, process.toString()) + + } catch (e: Exception) { + Log.d(ANG_PACKAGE, e.toString()) + } + } + + fun stopProcess() { + try { + Log.d(ANG_PACKAGE, "runProcess destroy") + process?.destroy() + } catch (e: Exception) { + Log.d(ANG_PACKAGE, e.toString()) + } + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt similarity index 79% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt index 1c9afe6cba..151d5c38e0 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt @@ -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), Context.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() { diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/ServiceControl.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/ServiceControl.kt similarity index 100% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/ServiceControl.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/service/ServiceControl.kt diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/SubscriptionUpdater.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdater.kt similarity index 82% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/SubscriptionUpdater.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdater.kt index 449b8835ca..4de3381bfb 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/SubscriptionUpdater.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdater.kt @@ -14,9 +14,8 @@ import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME import com.v2ray.ang.R -import com.v2ray.ang.util.AngConfigManager -import com.v2ray.ang.util.MmkvManager -import com.v2ray.ang.util.Utils +import com.v2ray.ang.handler.AngConfigManager.updateConfigViaSub +import com.v2ray.ang.handler.MmkvManager object SubscriptionUpdater { @@ -40,8 +39,8 @@ object SubscriptionUpdater { val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate } - for (i in subs) { - val subscription = i.second + for (sub in subs) { + val subItem = sub.second if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { notification.setChannelId(SUBSCRIPTION_UPDATE_CHANNEL) @@ -56,11 +55,10 @@ object SubscriptionUpdater { notificationManager.notify(3, notification.build()) Log.d( AppConfig.ANG_PACKAGE, - "subscription automatic update: ---${subscription.remarks}" + "subscription automatic update: ---${subItem.remarks}" ) - val configs = Utils.getUrlContentWithCustomUserAgent(subscription.url) - AngConfigManager.importBatchConfig(configs, i.first, false) - notification.setContentText("Updating ${subscription.remarks}") + updateConfigViaSub(Pair(sub.first, subItem)) + notification.setContentText("Updating ${subItem.remarks}") } notificationManager.cancel(3) return Result.success() diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayProxyOnlyService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt similarity index 100% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayProxyOnlyService.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt similarity index 76% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt index 55ec9a27b0..5b47447133 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt @@ -4,6 +4,7 @@ import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent +import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -13,24 +14,28 @@ import android.os.Build import android.util.Log import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat -import com.tencent.mmkv.MMKV +import androidx.core.content.ContextCompat import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig.TAG_DIRECT +import com.v2ray.ang.AppConfig.VPN import com.v2ray.ang.R -import com.v2ray.ang.dto.ServerConfig +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.extension.toSpeedString import com.v2ray.ang.extension.toast +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.V2rayConfigManager import com.v2ray.ang.ui.MainActivity import com.v2ray.ang.util.MessageUtil -import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.util.PluginUtil import com.v2ray.ang.util.Utils -import com.v2ray.ang.util.V2rayConfigUtil import go.Seq import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.Disposable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.launch import libv2ray.Libv2ray import libv2ray.V2RayPoint @@ -42,12 +47,11 @@ object V2RayServiceManager { private const val NOTIFICATION_ID = 1 private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0 private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1 + private const val NOTIFICATION_PENDING_INTENT_RESTART_V2RAY = 2 private const val NOTIFICATION_ICON_THRESHOLD = 3000 val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) private val mMsgReceive = ReceiveMessageHandler() - private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) } - private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } var serviceControl: SoftReference? = null set(value) { @@ -55,7 +59,7 @@ object V2RayServiceManager { Seq.setContext(value?.get()?.getService()?.applicationContext) Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey()) } - var currentConfig: ServerConfig? = null + var currentConfig: ProfileItem? = null private var lastQueryTime = 0L private var mBuilder: NotificationCompat.Builder? = null @@ -64,16 +68,21 @@ object V2RayServiceManager { fun startV2Ray(context: Context) { if (v2rayPoint.isRunning) return - val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return - val result = V2rayConfigUtil.getV2rayConfig(context, guid) - if (!result.status) return - - if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) { + val guid = MmkvManager.getSelectServer() ?: return + val config = MmkvManager.decodeServerConfig(guid) ?: return + if (config.configType != EConfigType.CUSTOM + && !Utils.isValidUrl(config.server) + && !Utils.isIpAddress(config.server) + ) return +// val result = V2rayConfigUtil.getV2rayConfig(context, guid) +// if (!result.status) return + + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) == true) { context.toast(R.string.toast_warning_pref_proxysharing_short) } else { context.toast(R.string.toast_services_start) } - val intent = if ((settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN") == "VPN") { + val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) { Intent(context.applicationContext, V2RayVpnService::class.java) } else { Intent(context.applicationContext, V2RayProxyOnlyService::class.java) @@ -108,13 +117,11 @@ object V2RayServiceManager { } override fun onEmitStatus(l: Long, s: String?): Long { - //Logger.d(s) return 0 } override fun setup(s: String): Long { val serviceControl = serviceControl?.get() ?: return -1 - //Logger.d(s) return try { serviceControl.startService() lastQueryTime = System.currentTimeMillis() @@ -127,14 +134,19 @@ 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 = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return + val guid = MmkvManager.getSelectServer() ?: return val config = MmkvManager.decodeServerConfig(guid) ?: return if (v2rayPoint.isRunning) { return } - val result = V2rayConfigUtil.getV2rayConfig(service, guid) + val result = V2rayConfigManager.getV2rayConfig(service, guid) if (!result.status) return @@ -143,22 +155,18 @@ 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()) } v2rayPoint.configureFileContent = result.content - v2rayPoint.domainName = config.getV2rayPointDomainAndPort() + v2rayPoint.domainName = result.domainPort currentConfig = config try { - v2rayPoint.runLoop(settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) ?: false) + v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) } catch (e: Exception) { Log.d(ANG_PACKAGE, e.toString()) } @@ -166,6 +174,8 @@ object V2RayServiceManager { if (v2rayPoint.isRunning) { MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "") showNotification() + + PluginUtil.runPlugin(service, config, result.domainPort) } else { MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "") cancelNotification() @@ -194,7 +204,7 @@ object V2RayServiceManager { } catch (e: Exception) { Log.d(ANG_PACKAGE, e.toString()) } - + PluginUtil.stopPlugin() } private class ReceiveMessageHandler : BroadcastReceiver() { @@ -202,7 +212,6 @@ object V2RayServiceManager { val serviceControl = serviceControl?.get() ?: return when (intent?.getIntExtra("key", 0)) { AppConfig.MSG_REGISTER_CLIENT -> { - //Logger.e("ReceiveMessageHandler", intent?.getIntExtra("key", 0).toString()) if (v2rayPoint.isRunning) { MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "") } else { @@ -219,11 +228,15 @@ object V2RayServiceManager { } AppConfig.MSG_STATE_STOP -> { + Log.d(ANG_PACKAGE, "Stop Service") serviceControl.stopService() } AppConfig.MSG_STATE_RESTART -> { - startV2rayPoint() + Log.d(ANG_PACKAGE, "Restart Service") + serviceControl.stopService() + Thread.sleep(500L) + startV2Ray(serviceControl.getService()) } AppConfig.MSG_MEASURE_DELAY -> { @@ -245,31 +258,44 @@ object V2RayServiceManager { } } + + private fun f35(flag:Boolean): Pair { + var time = -1L + var errstr = "" + try{ + time = v2rayPoint.measureDelay(Utils.getDelayTestUrl(flag)) + }catch (e: Exception) { + errstr = e.message?.substringAfter("\":") ?: "empty message" + } + return Pair(time,errstr) + } + private fun measureV2rayDelay() { CoroutineScope(Dispatchers.IO).launch { val service = serviceControl?.get()?.getService() ?: return@launch - var time = -1L - var errstr = "" + + var t1 = -1L + var t2 = -1L + var estr1 = "" if (v2rayPoint.isRunning) { try { - time = v2rayPoint.measureDelay(Utils.getDelayTestUrl()) + val g1 = async { f35(false) } + val g2 = async { f35(true) } + + val r1 = g1.await() + val r2 = g2.await() + t1 = r1.first + estr1 = r1.second + t2 = r2.first } catch (e: Exception) { - Log.d(ANG_PACKAGE, "measureV2rayDelay: $e") - errstr = e.message?.substringAfter("\":") ?: "empty message" - } - if (time == -1L) { - try { - time = v2rayPoint.measureDelay(Utils.getDelayTestUrl(true)) - } catch (e: Exception) { - Log.d(ANG_PACKAGE, "measureV2rayDelay: $e") - errstr = e.message?.substringAfter("\":") ?: "empty message" - } + Log.d(ANG_PACKAGE, "err: $e"); } } - val result = if (time == -1L) { - service.getString(R.string.connection_test_error, errstr) + + val result = if ( (t1 == -1L) && (t2 == -1L) ) { + service.getString(R.string.connection_test_error, estr1) } else { - service.getString(R.string.connection_test_available, time) + service.getString(R.string.connection_test_available, t1, t2) } MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result) @@ -278,30 +304,24 @@ object V2RayServiceManager { private fun showNotification() { val service = serviceControl?.get()?.getService() ?: return + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + val startMainIntent = Intent(service, MainActivity::class.java) - val contentPendingIntent = PendingIntent.getActivity( - service, - NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - } else { - PendingIntent.FLAG_UPDATE_CURRENT - } - ) + val contentPendingIntent = PendingIntent.getActivity(service, NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, flags) val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE) stopV2RayIntent.`package` = ANG_PACKAGE stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP) + val stopV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, flags) - val stopV2RayPendingIntent = PendingIntent.getBroadcast( - service, - NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - } else { - PendingIntent.FLAG_UPDATE_CURRENT - } - ) + val restartV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE) + restartV2RayIntent.`package` = ANG_PACKAGE + restartV2RayIntent.putExtra("key", AppConfig.MSG_STATE_RESTART) + val restartV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_RESTART_V2RAY, restartV2RayIntent, flags) val channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -325,6 +345,11 @@ object V2RayServiceManager { service.getString(R.string.notification_action_stop_v2ray), stopV2RayPendingIntent ) + .addAction( + R.drawable.ic_delete_24dp, + service.getString(R.string.title_service_restart), + restartV2RayPendingIntent + ) //.build() //mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使 @@ -349,7 +374,12 @@ object V2RayServiceManager { fun cancelNotification() { val service = serviceControl?.get()?.getService() ?: return - service.stopForeground(true) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + service.stopForeground(Service.STOP_FOREGROUND_REMOVE) + } else { + service.stopForeground(true) + } + mBuilder = null mDisposable?.dispose() mDisposable = null @@ -381,7 +411,7 @@ object V2RayServiceManager { private fun startSpeedNotification() { if (mDisposable == null && v2rayPoint.isRunning && - settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true + MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true ) { var lastZeroSpeed = false val outboundTags = currentConfig?.getAllOutboundTags() @@ -431,10 +461,11 @@ object V2RayServiceManager { } private fun stopSpeedNotification() { - if (mDisposable != null) { - mDisposable?.dispose() //stop queryStats + mDisposable?.let { + it.dispose() //stop queryStats mDisposable = null updateNotification(currentConfig?.remarks, 0, 0) } } + } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayTestService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt similarity index 59% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayTestService.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt index 6d6dc0aa9d..da80518bf0 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayTestService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt @@ -6,7 +6,12 @@ import android.os.IBinder import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.extension.serializable +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.V2rayConfigManager import com.v2ray.ang.util.MessageUtil +import com.v2ray.ang.util.PluginUtil import com.v2ray.ang.util.SpeedtestUtil import com.v2ray.ang.util.Utils import go.Seq @@ -19,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() @@ -30,10 +35,10 @@ class V2RayTestService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { when (intent?.getIntExtra("key", 0)) { MSG_MEASURE_CONFIG -> { - val contentPair = intent.getSerializableExtra("content") as Pair + val guid = intent.serializable("content") ?: "" realTestScope.launch { - val result = SpeedtestUtil.realPing(contentPair.second) - MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(contentPair.first, result)) + val result = startRealPing(guid) + MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result)) } } @@ -47,4 +52,20 @@ class V2RayTestService : Service() { override fun onBind(intent: Intent?): IBinder? { return null } + + private fun startRealPing(guid: String): Long { + val retFailure = -1L + + val config = MmkvManager.decodeServerConfig(guid) ?: return retFailure + if (config.configType == EConfigType.HYSTERIA2) { + val delay = PluginUtil.realPingHy2(this, config) + return delay + } else { + val config = V2rayConfigManager.getV2rayConfig(this, guid) + if (!config.status) { + return retFailure + } + return SpeedtestUtil.realPing(Utils.removeKeepAliveAndRouting(config.content)) + } + } } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt similarity index 80% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt index c5f3ea9974..41a5745d41 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt @@ -10,17 +10,20 @@ import android.net.LocalSocketAddress import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest +import android.net.ProxyInfo import android.net.VpnService import android.os.Build import android.os.ParcelFileDescriptor import android.os.StrictMode import android.util.Log import androidx.annotation.RequiresApi -import com.tencent.mmkv.MMKV 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 -import com.v2ray.ang.dto.ERoutingMode -import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.util.MyContextWrapper import com.v2ray.ang.util.Utils import kotlinx.coroutines.CoroutineScope @@ -32,14 +35,13 @@ import java.lang.ref.SoftReference class V2RayVpnService : VpnService(), ServiceControl { companion object { private const val VPN_MTU = 1500 - private const val PRIVATE_VLAN4_CLIENT = "26.26.26.1" - private const val PRIVATE_VLAN4_ROUTER = "26.26.26.2" - private const val PRIVATE_VLAN6_CLIENT = "da26:2626::1" - private const val PRIVATE_VLAN6_ROUTER = "da26:2626::2" + private const val PRIVATE_VLAN4_CLIENT = "10.10.10.1" + private const val PRIVATE_VLAN4_ROUTER = "10.10.10.2" + private const val PRIVATE_VLAN6_CLIENT = "fc00::10:10:10:1" + private const val PRIVATE_VLAN6_ROUTER = "fc00::10:10:10:2" private const val TUN2SOCKS = "libtun2socks.so" } - private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } private lateinit var mInterface: ParcelFileDescriptor private var isRunning = false @@ -64,7 +66,7 @@ class V2RayVpnService : VpnService(), ServiceControl { .build() } - private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } + private val connectivity by lazy { getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager } @delegate:RequiresApi(Build.VERSION_CODES.P) private val defaultNetworkCallback by lazy { @@ -117,13 +119,11 @@ class V2RayVpnService : VpnService(), ServiceControl { val builder = Builder() //val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false) - val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) - ?: ERoutingMode.BYPASS_LAN_MAINLAND.value - builder.setMtu(VPN_MTU) builder.addAddress(PRIVATE_VLAN4_CLIENT, 30) //builder.addDnsServer(PRIVATE_VLAN4_ROUTER) - if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) { + val bypassLan = SettingsManager.routingRulesetsBypassLan() + if (bypassLan) { resources.getStringArray(R.array.bypass_private_ip_address).forEach { val addr = it.split('/') builder.addRoute(addr[0], addr[1].toInt()) @@ -132,31 +132,34 @@ class V2RayVpnService : VpnService(), ServiceControl { builder.addRoute("0.0.0.0", 0) } - if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) { builder.addAddress(PRIVATE_VLAN6_CLIENT, 126) - if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) { + if (bypassLan) { builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use } else { builder.addRoute("::", 0) } } - if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { - builder.addDnsServer(PRIVATE_VLAN4_ROUTER) - } else { - Utils.getVpnDnsServers() - .forEach { - if (Utils.isPureIpAddress(it)) { - builder.addDnsServer(it) - } +// if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { +// builder.addDnsServer(PRIVATE_VLAN4_ROUTER) +// } else { + Utils.getVpnDnsServers() + .forEach { + if (Utils.isPureIpAddress(it)) { + builder.addDnsServer(it) } - } + } +// } builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty()) - if (settingsStorage?.decodeBool(AppConfig.PREF_PER_APP_PROXY) == true) { - val apps = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET) - val bypassApps = settingsStorage?.decodeBool(AppConfig.PREF_BYPASS_APPS) ?: false + val selfPackageName = BuildConfig.APPLICATION_ID + 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) + //process self package + if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName) apps?.forEach { try { if (bypassApps) @@ -164,9 +167,11 @@ class V2RayVpnService : VpnService(), ServiceControl { else builder.addAllowedApplication(it) } catch (e: PackageManager.NameNotFoundException) { - //Logger.d(e) + Log.d(ANG_PACKAGE, "setup error : --${e.localizedMessage}") } } + } else { + builder.addDisallowedApplication(selfPackageName) } // Close the old interface since the parameters have been changed. @@ -186,6 +191,9 @@ class V2RayVpnService : VpnService(), ServiceControl { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { builder.setMetered(false) + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY)) { + builder.setHttpProxy(ProxyInfo.buildDirectProxy(LOOPBACK, SettingsManager.getHttpPort())) + } } // Create a new interface using the builder and save the parameters. @@ -201,26 +209,26 @@ class V2RayVpnService : VpnService(), ServiceControl { } private fun runTun2socks() { - val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt()) + val socksPort = SettingsManager.getSocksPort() val cmd = arrayListOf( File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath, "--netif-ipaddr", PRIVATE_VLAN4_ROUTER, "--netif-netmask", "255.255.255.252", - "--socks-server-addr", "127.0.0.1:${socksPort}", + "--socks-server-addr", "$LOOPBACK:${socksPort}", "--tunmtu", VPN_MTU.toString(), "--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath, "--enable-udprelay", "--loglevel", "notice" ) - if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) { cmd.add("--netif-ip6addr") cmd.add(PRIVATE_VLAN6_ROUTER) } - if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { - val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt()) + 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("127.0.0.1:${localDnsPort}") + cmd.add("$LOOPBACK:${localDnsPort}") } Log.d(packageName, cmd.toString()) @@ -230,7 +238,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") @@ -238,7 +246,7 @@ class V2RayVpnService : VpnService(), ServiceControl { Log.d(packageName, "$TUN2SOCKS restart") runTun2socks() } - }).start() + }.start() Log.d(packageName, process.toString()) sendFd() diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/AboutActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt similarity index 79% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/AboutActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt index 8f0f1bf0d6..eacacb3558 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/AboutActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt @@ -4,6 +4,7 @@ import android.Manifest import android.content.Intent import android.os.Build import android.os.Bundle +import android.util.Log import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.FileProvider import com.tbruyelle.rxpermissions3.RxPermissions @@ -20,6 +21,7 @@ import java.io.File import java.text.SimpleDateFormat import java.util.Locale + class AboutActivity : BaseActivity() { private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) } private val extDir by lazy { File(Utils.backupPath(this)) } @@ -86,6 +88,14 @@ class AboutActivity : BaseActivity() { binding.layoutFeedback.setOnClickListener { Utils.openUri(this, AppConfig.v2rayNGIssues) } + binding.layoutOssLicenses.setOnClickListener{ + val webView = android.webkit.WebView(this); + webView.loadUrl("file:///android_asset/open_source_licenses.html"); + android.app.AlertDialog.Builder(this) + .setTitle("Open source licenses") + .setView(webView) + .setPositiveButton("OK", android.content.DialogInterface.OnClickListener { dialog, whichButton -> dialog.dismiss() }).show() + } binding.layoutTgChannel.setOnClickListener { Utils.openUri(this, AppConfig.TgChannelUrl) @@ -133,13 +143,15 @@ class AboutActivity : BaseActivity() { } private fun showFileChooser() { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.type = "*/*" - intent.addCategory(Intent.CATEGORY_OPENABLE) + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + type = "*/*" + addCategory(Intent.CATEGORY_OPENABLE) + } try { chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) } catch (ex: android.content.ActivityNotFoundException) { + Log.e(AppConfig.ANG_PACKAGE, "File chooser activity not found: ${ex.message}", ex) toast(R.string.toast_require_file_manager) } } @@ -149,27 +161,23 @@ class AboutActivity : BaseActivity() { val uri = it.data?.data if (it.resultCode == RESULT_OK && uri != null) { try { - try { - val targetFile = - File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip") - contentResolver.openInputStream(uri).use { input -> - targetFile.outputStream().use { fileOut -> - input?.copyTo(fileOut) - } - } - if (restoreConfiguration(targetFile)) { - toast(R.string.toast_success) - } else { - toast(R.string.toast_failure) + val targetFile = + File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip") + contentResolver.openInputStream(uri).use { input -> + targetFile.outputStream().use { fileOut -> + input?.copyTo(fileOut) } - } catch (e: Exception) { - e.printStackTrace() } - + if (restoreConfiguration(targetFile)) { + toast(R.string.toast_success) + } else { + toast(R.string.toast_failure) + } } catch (e: Exception) { - e.printStackTrace() - toast(e.message.toString()) + Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e) + toast(R.string.toast_failure) } } } + } \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt similarity index 89% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt index 8a2c7fda8b..f2d4ba9411 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt @@ -34,9 +34,6 @@ abstract class BaseActivity : AppCompatActivity() { @RequiresApi(Build.VERSION_CODES.N) override fun attachBaseContext(newBase: Context?) { - val context = newBase?.let { - MyContextWrapper.wrap(newBase, Utils.getLocale()) - } - super.attachBaseContext(context) + super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, Utils.getLocale())) } } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/FragmentAdapter.kt similarity index 100% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/FragmentAdapter.kt diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt new file mode 100644 index 0000000000..b275c348ee --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt @@ -0,0 +1,149 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.widget.SearchView +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivityLogcatBinding +import com.v2ray.ang.extension.toast +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.IOException + + +class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener { + private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) } + + var logsetsAll: MutableList = mutableListOf() + var logsets: MutableList = mutableListOf() + private val adapter by lazy { LogcatRecyclerAdapter(this) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + title = getString(R.string.title_logcat) + + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = adapter + binding.recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) + + binding.refreshLayout.setOnRefreshListener(this) + + logsets.add(getString(R.string.pull_down_to_refresh)) + } + + private fun getLogcat() { + + try { + binding.refreshLayout.isRefreshing = true + + lifecycleScope.launch(Dispatchers.Default) { + val lst = LinkedHashSet() + lst.add("logcat") + lst.add("-d") + lst.add("-v") + lst.add("time") + lst.add("-s") + lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err") + val process = withContext(Dispatchers.IO) { + Runtime.getRuntime().exec(lst.toTypedArray()) + } + + val allText = process.inputStream.bufferedReader().use { it.readLines() }.reversed() + launch(Dispatchers.Main) { + logsetsAll = allText.toMutableList() + logsets = allText.toMutableList() + adapter.notifyDataSetChanged() + binding.refreshLayout.isRefreshing = false + } + } + } catch (e: IOException) { + e.printStackTrace() + } + } + + private fun clearLogcat() { + try { + lifecycleScope.launch(Dispatchers.Default) { + val lst = LinkedHashSet() + lst.add("logcat") + lst.add("-c") + withContext(Dispatchers.IO) { + val process = Runtime.getRuntime().exec(lst.toTypedArray()) + process.waitFor() + } + launch(Dispatchers.Main) { + logsetsAll.clear() + logsets.clear() + adapter.notifyDataSetChanged() + } + } + } catch (e: IOException) { + e.printStackTrace() + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_logcat, menu) + + val searchItem = menu.findItem(R.id.search_view) + if (searchItem != null) { + val searchView = searchItem.actionView as SearchView + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean = false + + override fun onQueryTextChange(newText: String?): Boolean { + filterLogs(newText) + return false + } + }) + searchView.setOnCloseListener { + filterLogs("") + false + } + } + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.copy_all -> { + Utils.setClipboard(this, logsets.joinToString("\n")) + toast(R.string.toast_success) + true + } + + R.id.clear_all -> { + clearLogcat() + true + } + + else -> super.onOptionsItemSelected(item) + } + + private fun filterLogs(content: String?): Boolean { + val key = content?.trim() + logsets = if (key.isNullOrEmpty()) { + logsetsAll.toMutableList() + } else { + logsetsAll.filter { it.contains(key) }.toMutableList() + } + + adapter?.notifyDataSetChanged() + return true + } + + override fun onRefresh() { + getLogcat() + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatRecyclerAdapter.kt new file mode 100644 index 0000000000..4a9c35fa85 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatRecyclerAdapter.kt @@ -0,0 +1,31 @@ +package com.v2ray.ang.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding + +class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter() { + private var mActivity: LogcatActivity = activity + + override fun getItemCount() = mActivity.logsets.size + + override fun onBindViewHolder(holder: MainViewHolder, position: Int) { + val content = mActivity.logsets[position] + holder.itemSubSettingBinding.logContent.text = content + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder { + return MainViewHolder( + ItemRecyclerLogcatBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + class MainViewHolder(val itemSubSettingBinding: ItemRecyclerLogcatBinding) : RecyclerView.ViewHolder(itemSubSettingBinding.root) + +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt similarity index 88% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt index a4b766fce0..37b27e944f 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt @@ -29,14 +29,16 @@ import com.google.android.material.navigation.NavigationView import com.google.android.material.tabs.TabLayout import com.tbruyelle.rxpermissions3.RxPermissions import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.VPN import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityMainBinding import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.extension.toast +import com.v2ray.ang.handler.AngConfigManager +import com.v2ray.ang.handler.MigrateManager +import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.helper.SimpleItemTouchHelperCallback import com.v2ray.ang.service.V2RayServiceManager -import com.v2ray.ang.util.AngConfigManager -import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.Utils import com.v2ray.ang.viewmodel.MainViewModel import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers @@ -44,6 +46,7 @@ import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import me.drakeet.support.toast.ToastCompat import java.util.concurrent.TimeUnit @@ -87,7 +90,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList binding.fab.setOnClickListener { if (mainViewModel.isRunning.value == true) { Utils.stopVService(this) - } else if ((MmkvManager.settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN") == "VPN") { + } else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) { val intent = VpnService.prepare(this) if (intent == null) { startV2Ray() @@ -111,11 +114,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.adapter = adapter - val callback = SimpleItemTouchHelperCallback(adapter) - mItemTouchHelper = ItemTouchHelper(callback) + mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) mItemTouchHelper?.attachToRecyclerView(binding.recyclerView) - val toggle = ActionBarDrawerToggle( this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close ) @@ -125,13 +126,14 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList initGroupTab() setupViewModel() + migrateLegacy() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { RxPermissions(this) .request(Manifest.permission.POST_NOTIFICATIONS) .subscribe { if (!it) - toast(R.string.toast_permission_denied) + toast(R.string.toast_permission_denied_notification) } } @@ -171,7 +173,22 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList } } mainViewModel.startListenBroadcast() - mainViewModel.copyAssets(assets) + mainViewModel.initAssets(assets) + } + + private fun migrateLegacy() { + lifecycleScope.launch(Dispatchers.IO) { + val result = MigrateManager.migrateServerConfig2Profile() + launch(Dispatchers.Main) { + if (result) { + toast(getString(R.string.migration_success)) + mainViewModel.reloadServerList() + } else { + //toast(getString(R.string.migration_fail)) + } + } + + } } private fun initGroupTab() { @@ -198,7 +215,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList } fun startV2Ray() { - if (MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) { + if (MmkvManager.getSelectServer().isNullOrEmpty()) { + toast(R.string.title_file_chooser) return } V2RayServiceManager.startV2Ray(this) @@ -278,6 +296,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList true } + R.id.import_manually_http -> { + importManually(EConfigType.HTTP.value) + true + } + R.id.import_manually_trojan -> { importManually(EConfigType.TROJAN.value) true @@ -288,6 +311,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList true } + R.id.import_manually_hysteria2 -> { + importManually(EConfigType.HYSTERIA2.value) + true + } + R.id.import_config_custom_clipboard -> { importConfigCustomClipboard() true @@ -318,8 +346,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList lifecycleScope.launch(Dispatchers.IO) { val ret = mainViewModel.exportAllServer() launch(Dispatchers.Main) { - if (ret == 0) - toast(R.string.toast_success) + if (ret > 0) + toast(getString(R.string.title_export_config_count, ret)) else toast(R.string.toast_failure) binding.pbWaiting.hide() @@ -330,11 +358,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList } R.id.ping_all -> { + toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count())) mainViewModel.testAllTcping() true } R.id.real_ping_all -> { + toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count())) mainViewModel.testAllRealPing() true } @@ -349,14 +379,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList .setPositiveButton(android.R.string.ok) { _, _ -> binding.pbWaiting.show() lifecycleScope.launch(Dispatchers.IO) { - mainViewModel.removeAllServer() + val ret = mainViewModel.removeAllServer() launch(Dispatchers.Main) { mainViewModel.reloadServerList() + toast(getString(R.string.title_del_config_count, ret)) binding.pbWaiting.hide() } } } - .setNegativeButton(android.R.string.no) { _, _ -> + .setNegativeButton(android.R.string.cancel) { _, _ -> //do noting } .show() @@ -376,7 +407,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList } } } - .setNegativeButton(android.R.string.no) { _, _ -> + .setNegativeButton(android.R.string.cancel) { _, _ -> //do noting } .show() @@ -388,14 +419,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList .setPositiveButton(android.R.string.ok) { _, _ -> binding.pbWaiting.show() lifecycleScope.launch(Dispatchers.IO) { - mainViewModel.removeInvalidServer() + val ret = mainViewModel.removeInvalidServer() launch(Dispatchers.Main) { mainViewModel.reloadServerList() + toast(getString(R.string.title_del_config_count, ret)) binding.pbWaiting.hide() } } } - .setNegativeButton(android.R.string.no) { _, _ -> + .setNegativeButton(android.R.string.cancel) { _, _ -> //do noting } .show() @@ -478,30 +510,35 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList } private fun importBatchConfig(server: String?) { -// val dialog = AlertDialog.Builder(this) -// .setView(LayoutProgressBinding.inflate(layoutInflater).root) -// .setCancelable(false) -// .show() binding.pbWaiting.show() lifecycleScope.launch(Dispatchers.IO) { - val (count, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true) - delay(500L) - launch(Dispatchers.Main) { - if (count > 0) { - toast(R.string.toast_success) - mainViewModel.reloadServerList() - } else if (countSub > 0) { - initGroupTab() - } else { + try { + val (count, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true) + delay(500L) + withContext(Dispatchers.Main) { + when { + count > 0 -> { + toast(getString(R.string.title_import_config_count, count)) + mainViewModel.reloadServerList() + } + + countSub > 0 -> initGroupTab() + else -> toast(R.string.toast_failure) + } + binding.pbWaiting.hide() + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { toast(R.string.toast_failure) + binding.pbWaiting.hide() } - //dialog.dismiss() - binding.pbWaiting.hide() + e.printStackTrace() } } } + private fun importConfigCustomClipboard() : Boolean { try { @@ -588,7 +625,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList delay(500L) launch(Dispatchers.Main) { if (count > 0) { - toast(R.string.toast_success) + toast(getString(R.string.title_update_config_count, count)) mainViewModel.reloadServerList() } else { toast(R.string.toast_failure) @@ -706,10 +743,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList ) } - R.id.user_asset_setting -> { - startActivity(Intent(this, UserAssetActivity::class.java)) + R.id.routing_setting -> { + requestSubSettingActivity.launch(Intent(this, RoutingSettingActivity::class.java)) } + R.id.promotion -> { Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}") } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt similarity index 82% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt index 711217cff0..7c657a9e7c 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt @@ -9,7 +9,6 @@ import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView -import com.google.gson.Gson import com.v2ray.ang.AngApplication.Companion.application import com.v2ray.ang.AppConfig import com.v2ray.ang.R @@ -17,13 +16,12 @@ import com.v2ray.ang.databinding.ItemQrcodeBinding import com.v2ray.ang.databinding.ItemRecyclerFooterBinding import com.v2ray.ang.databinding.ItemRecyclerMainBinding import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.extension.toast +import com.v2ray.ang.handler.AngConfigManager +import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.helper.ItemTouchHelperAdapter import com.v2ray.ang.helper.ItemTouchHelperViewHolder import com.v2ray.ang.service.V2RayServiceManager -import com.v2ray.ang.util.AngConfigManager -import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.Utils import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable @@ -66,17 +64,12 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter { - holder.itemMainBinding.tvType.text = profile.configType.name - } - else -> { - holder.itemMainBinding.tvType.text = profile.configType.name.lowercase() + holder.itemMainBinding.tvType.text = profile.configType.name } } - val strState = "${profile.server?.dropLast(3)}*** : ${profile.serverPort}" + // 隐藏主页服务器地址为xxx:xxx:***/xxx.xxx.xxx.*** + val strState = "${ + profile.server?.let { + if (it.contains(":")) + it.split(":").take(2).joinToString(":", postfix = ":***") + else + it.split('.').dropLast(1).joinToString(".", postfix = ".***") + } + } : ${profile.serverPort}" holder.itemMainBinding.tvStatistics.text = strState @@ -132,6 +129,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter removeServer(guid, position) } - .setNegativeButton(android.R.string.no) { _, _ -> + .setNegativeButton(android.R.string.cancel) { _, _ -> //do noting } .show() @@ -158,9 +156,9 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter fromPosition) - notifyItemRangeChanged(fromPosition, toPosition - fromPosition + 1) - else - notifyItemRangeChanged(toPosition, fromPosition - toPosition + 1) return true } override fun onItemMoveCompleted() { // do nothing } + + override fun onItemDismiss(position: Int) { + } } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt similarity index 93% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt index 1e2b92bb4b..b1a54db50a 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt @@ -10,7 +10,6 @@ import androidx.appcompat.widget.SearchView import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager -import com.tencent.mmkv.MMKV import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.R @@ -18,8 +17,8 @@ import com.v2ray.ang.databinding.ActivityBypassListBinding import com.v2ray.ang.dto.AppInfo import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.v2RayApplication +import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.util.AppManagerUtil -import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.Utils import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.schedulers.Schedulers @@ -34,7 +33,6 @@ class PerAppProxyActivity : BaseActivity() { private var adapter: PerAppProxyAdapter? = null private var appsAll: List? = null - private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -43,7 +41,7 @@ class PerAppProxyActivity : BaseActivity() { val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL) binding.recyclerView.addItemDecoration(dividerItemDecoration) - val blacklist = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET) + val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET) AppManagerUtil.rxLoadNetworkAppList(this) .subscribeOn(Schedulers.io()) @@ -134,14 +132,14 @@ class PerAppProxyActivity : BaseActivity() { ***/ binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked -> - settingsStorage.encode(AppConfig.PREF_PER_APP_PROXY, isChecked) + MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked) } - binding.switchPerAppProxy.isChecked = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false) + binding.switchPerAppProxy.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false) binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked -> - settingsStorage.encode(AppConfig.PREF_BYPASS_APPS, isChecked) + MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked) } - binding.switchBypassApps.isChecked = settingsStorage.getBoolean(AppConfig.PREF_BYPASS_APPS, false) + binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false) /*** et_search.setOnEditorActionListener { v, actionId, event -> @@ -177,7 +175,7 @@ class PerAppProxyActivity : BaseActivity() { override fun onPause() { super.onPause() adapter?.let { - settingsStorage.encode(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist) + MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist) } } @@ -217,7 +215,7 @@ class PerAppProxyActivity : BaseActivity() { } it.notifyDataSetChanged() true - } ?: false + } == true R.id.select_proxy_app -> { selectProxyApp() diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyAdapter.kt similarity index 90% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyAdapter.kt index 7e5dcddde7..deb585fea7 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyAdapter.kt @@ -59,22 +59,23 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List, bl fun bind(appInfo: AppInfo) { this.appInfo = appInfo + // Set app icon and name itemBypassBinding.icon.setImageDrawable(appInfo.appIcon) -// name.text = appInfo.appName - - itemBypassBinding.checkBox.isChecked = inBlacklist - itemBypassBinding.packageName.text = appInfo.packageName - if (appInfo.isSystemApp) { - itemBypassBinding.name.text = String.format("** %1s", appInfo.appName) - //name.textColor = Color.RED + itemBypassBinding.name.text = if (appInfo.isSystemApp) { + String.format("** %s", appInfo.appName) } else { - itemBypassBinding.name.text = appInfo.appName - //name.textColor = Color.DKGRAY + appInfo.appName } + // Set package name and checkbox state + itemBypassBinding.packageName.text = appInfo.packageName + itemBypassBinding.checkBox.isChecked = inBlacklist + + // Handle item click to toggle blacklist status itemView.setOnClickListener(this) } + override fun onClick(v: View?) { if (inBlacklist) { blacklist.remove(appInfo.packageName) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt new file mode 100644 index 0000000000..561db0e966 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt @@ -0,0 +1,131 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivityRoutingEditBinding +import com.v2ray.ang.dto.RulesetItem +import com.v2ray.ang.extension.toast +import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class RoutingEditActivity : BaseActivity() { + private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) } + private val position by lazy { intent.getIntExtra("position", -1) } + + private val outbound_tag: Array by lazy { + resources.getStringArray(R.array.outbound_tag) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + title = getString(R.string.routing_settings_rule_title) + + val rulesetItem = SettingsManager.getRoutingRuleset(position) + if (rulesetItem != null) { + bindingServer(rulesetItem) + } else { + clearServer() + } + } + + private fun bindingServer(rulesetItem: RulesetItem): Boolean { + binding.etRemarks.text = Utils.getEditable(rulesetItem.remarks) + binding.chkLocked.isChecked = rulesetItem.locked == true + binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(",")) + binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(",")) + binding.etPort.text = Utils.getEditable(rulesetItem.port) + binding.etProtocol.text = Utils.getEditable(rulesetItem.protocol?.joinToString(",")) + binding.etNetwork.text = Utils.getEditable(rulesetItem.network) + val outbound = Utils.arrayFind(outbound_tag, rulesetItem.outboundTag) + binding.spOutboundTag.setSelection(outbound) + + return true + } + + private fun clearServer(): Boolean { + binding.etRemarks.text = null + binding.spOutboundTag.setSelection(0) + return true + } + + private fun saveServer(): Boolean { + val rulesetItem = SettingsManager.getRoutingRuleset(position) ?: RulesetItem() + + rulesetItem.apply { + remarks = binding.etRemarks.text.toString() + locked = binding.chkLocked.isChecked + domain = binding.etDomain.text.toString().takeIf { it.isNotEmpty() } + ?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } + ip = binding.etIp.text.toString().takeIf { it.isNotEmpty() } + ?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } + protocol = binding.etProtocol.text.toString().takeIf { it.isNotEmpty() } + ?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } + port = binding.etPort.text.toString().takeIf { it.isNotEmpty() } + network = binding.etNetwork.text.toString().takeIf { it.isNotEmpty() } + outboundTag = outbound_tag[binding.spOutboundTag.selectedItemPosition] + } + + if (rulesetItem.remarks.isNullOrEmpty()) { + toast(R.string.sub_setting_remarks) + return false + } + + SettingsManager.saveRoutingRuleset(position, rulesetItem) + toast(R.string.toast_success) + finish() + return true + } + + + private fun deleteServer(): Boolean { + if (position >= 0) { + AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + lifecycleScope.launch(Dispatchers.IO) { + SettingsManager.removeRoutingRuleset(position) + launch(Dispatchers.Main) { + finish() + } + } + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + // do nothing + } + .show() + } + return true + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.action_server, menu) + val del_config = menu.findItem(R.id.del_config) + + if (position < 0) { + del_config?.isVisible = false + } + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.del_config -> { + deleteServer() + true + } + + R.id.save_config -> { + saveServer() + true + } + + else -> super.onOptionsItemSelected(item) + } + +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt new file mode 100644 index 0000000000..44410d6735 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt @@ -0,0 +1,205 @@ +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 +import com.v2ray.ang.dto.RulesetItem +import com.v2ray.ang.extension.toast +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.helper.SimpleItemTouchHelperCallback +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class RoutingSettingActivity : BaseActivity() { + private val binding by lazy { ActivityRoutingSettingBinding.inflate(layoutInflater) } + + var rulesets: MutableList = mutableListOf() + private val adapter by lazy { RoutingSettingRecyclerAdapter(this) } + private var mItemTouchHelper: ItemTouchHelper? = null + private val routing_domain_strategy: Array by lazy { + resources.getStringArray(R.array.routing_domain_strategy) + } + private val preset_rulesets: Array by lazy { + resources.getStringArray(R.array.preset_rulesets) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + title = getString(R.string.routing_settings_title) + + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = adapter + + mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) + mItemTouchHelper?.attachToRecyclerView(binding.recyclerView) + + val found = Utils.arrayFind(routing_domain_strategy, MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "") + found.let { binding.spDomainStrategy.setSelection(if (it >= 0) it else 0) } + binding.spDomainStrategy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(parent: AdapterView<*>?) { + } + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, routing_domain_strategy[position]) + } + } + } + + override fun onResume() { + super.onResume() + refreshData() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_routing_setting, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.add_rule -> { + startActivity(Intent(this, RoutingEditActivity::class.java)) + true + } + + R.id.user_asset_setting -> { + startActivity(Intent(this, UserAssetActivity::class.java)) + true + } + + 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.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i) + launch(Dispatchers.Main) { + refreshData() + toast(R.string.toast_success) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + }.show() + + + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + //do noting + } + .show() + true + } + + R.id.import_rulesets_from_clipboard -> { + AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip) + .setPositiveButton(android.R.string.ok) { _, _ -> + val clipboard = try { + Utils.getClipboard(this) + } catch (e: Exception) { + e.printStackTrace() + toast(R.string.toast_failure) + return@setPositiveButton + } + lifecycleScope.launch(Dispatchers.IO) { + val result = SettingsManager.resetRoutingRulesets(clipboard) + withContext(Dispatchers.Main) { + if (result) { + refreshData() + toast(R.string.toast_success) + } else { + toast(R.string.toast_failure) + } + } + } + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + //do nothing + } + .show() + 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() + if (rulesetList.isNullOrEmpty()) { + toast(R.string.toast_failure) + } else { + Utils.setClipboard(this, JsonUtil.toJson(rulesetList)) + toast(R.string.toast_success) + } + true + } + + 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.cancel) { _, _ -> + //do nothing + } + .show() + return true + } + + fun refreshData() { + rulesets.clear() + rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf()) + adapter.notifyDataSetChanged() + } + +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingRecyclerAdapter.kt new file mode 100644 index 0000000000..bd48ea59f7 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingRecyclerAdapter.kt @@ -0,0 +1,80 @@ +package com.v2ray.ang.ui + +import android.content.Intent +import android.graphics.Color +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.v2ray.ang.databinding.ItemRecyclerRoutingSettingBinding +import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.helper.ItemTouchHelperAdapter +import com.v2ray.ang.helper.ItemTouchHelperViewHolder + +class RoutingSettingRecyclerAdapter(val activity: RoutingSettingActivity) : RecyclerView.Adapter(), + ItemTouchHelperAdapter { + + private var mActivity: RoutingSettingActivity = activity + override fun getItemCount() = mActivity.rulesets.size + + override fun onBindViewHolder(holder: MainViewHolder, position: Int) { + val ruleset = mActivity.rulesets[position] + + holder.itemRoutingSettingBinding.remarks.text = ruleset.remarks + holder.itemRoutingSettingBinding.domainIp.text = (ruleset.domain ?: ruleset.ip ?: ruleset.port)?.toString() + holder.itemRoutingSettingBinding.outboundTag.text = ruleset.outboundTag + holder.itemRoutingSettingBinding.chkEnable.isChecked = ruleset.enabled + holder.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.locked == true + holder.itemView.setBackgroundColor(Color.TRANSPARENT) + + holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener { + mActivity.startActivity( + Intent(mActivity, RoutingEditActivity::class.java) + .putExtra("position", position) + ) + } + + holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked -> + if (!it.isPressed) return@setOnCheckedChangeListener + ruleset.enabled = isChecked + SettingsManager.saveRoutingRuleset(position, ruleset) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder { + return MainViewHolder( + ItemRecyclerRoutingSettingBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + class MainViewHolder(val itemRoutingSettingBinding: ItemRecyclerRoutingSettingBinding) : + BaseViewHolder(itemRoutingSettingBinding.root), ItemTouchHelperViewHolder + + open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + fun onItemSelected() { + itemView.setBackgroundColor(Color.LTGRAY) + } + + fun onItemClear() { + itemView.setBackgroundColor(0) + } + } + + override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { + SettingsManager.swapRoutingRuleset(fromPosition, toPosition) + notifyItemMoved(fromPosition, toPosition) + return true + } + + override fun onItemMoveCompleted() { + mActivity.refreshData() + } + + override fun onItemDismiss(position: Int) { + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt similarity index 82% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt index 17090b75d2..187a6de3dc 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt @@ -7,7 +7,7 @@ import androidx.activity.result.contract.ActivityResultContracts import com.tbruyelle.rxpermissions3.RxPermissions import com.v2ray.ang.R import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.AngConfigManager +import com.v2ray.ang.handler.AngConfigManager class ScScannerActivity : BaseActivity() { @@ -20,26 +20,31 @@ class ScScannerActivity : BaseActivity() { fun importQRcode(): Boolean { RxPermissions(this) .request(Manifest.permission.CAMERA) - .subscribe { - if (it) + .subscribe { granted -> + if (granted) { scanQRCode.launch(Intent(this, ScannerActivity::class.java)) - else + } else { toast(R.string.toast_permission_denied) + } } - return true } + private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == RESULT_OK) { - val (count, countSub) = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false) + val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty() + val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false) + if (count + countSub > 0) { toast(R.string.toast_success) } else { toast(R.string.toast_failure) } + startActivity(Intent(this, MainActivity::class.java)) } finish() } + } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScSwitchActivity.kt similarity index 100% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScSwitchActivity.kt diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt similarity index 82% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt index 029f023b15..6402c5a255 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt @@ -9,11 +9,10 @@ import android.view.Menu import android.view.MenuItem import androidx.activity.result.contract.ActivityResultContracts import com.tbruyelle.rxpermissions3.RxPermissions -import com.tencent.mmkv.MMKV import com.v2ray.ang.AppConfig import com.v2ray.ang.R import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.util.QRCodeDecoder import io.github.g00fy2.quickie.QRResult import io.github.g00fy2.quickie.ScanCustomCode @@ -22,12 +21,11 @@ import io.github.g00fy2.quickie.config.ScannerConfig class ScannerActivity : BaseActivity() { private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult) - private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (settingsStorage?.decodeBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) { launchScan() } } @@ -76,19 +74,17 @@ class ScannerActivity : BaseActivity() { } RxPermissions(this) .request(permission) - .subscribe { - if (it) { - try { - showFileChooser() - } catch (e: Exception) { - e.printStackTrace() - } - } else + .subscribe { granted -> + if (granted) { + showFileChooser() + } else { toast(R.string.toast_permission_denied) + } } true } + else -> super.onOptionsItemSelected(item) } @@ -109,13 +105,21 @@ class ScannerActivity : BaseActivity() { val uri = it.data?.data if (it.resultCode == RESULT_OK && uri != null) { try { - val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri)) + val inputStream = contentResolver.openInputStream(uri) + val bitmap = BitmapFactory.decodeStream(inputStream) + inputStream?.close() + val text = QRCodeDecoder.syncDecodeQRCode(bitmap) - finished(text.orEmpty()) + if (text.isNullOrEmpty()) { + toast(R.string.toast_decoding_failed) + } else { + finished(text) + } } catch (e: Exception) { e.printStackTrace() - toast(e.message.toString()) + toast(R.string.toast_decoding_failed) } } } + } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt new file mode 100644 index 0000000000..f6812223ec --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt @@ -0,0 +1,684 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.Spinner +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.AppConfig.DEFAULT_PORT +import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE +import com.v2ray.ang.AppConfig.REALITY +import com.v2ray.ang.AppConfig.TLS +import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 +import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6 +import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU +import com.v2ray.ang.AppConfig.WIREGUARD_keep_alive +import com.v2ray.ang.AppConfig.WIREGUARD_wnoise +import com.v2ray.ang.AppConfig.WIREGUARD_wnoisecount +import com.v2ray.ang.AppConfig.WIREGUARD_wnoisedelay +import com.v2ray.ang.AppConfig.WIREGUARD_wpayloadsize +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 +import com.v2ray.ang.util.Utils + +class ServerActivity : BaseActivity() { + + private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() } + private val isRunning by lazy { + intent.getBooleanExtra("isRunning", false) + && editGuid.isNotEmpty() + && editGuid == MmkvManager.getSelectServer() + } + private val createConfigType by lazy { + EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value)) + ?: EConfigType.VMESS + } + private val subscriptionId by lazy { + intent.getStringExtra("subscriptionId") + } + + private val securitys: Array by lazy { + resources.getStringArray(R.array.securitys) + } + private val shadowsocksSecuritys: Array by lazy { + resources.getStringArray(R.array.ss_securitys) + } + private val flows: Array by lazy { + resources.getStringArray(R.array.flows) + } + private val networks: Array by lazy { + resources.getStringArray(R.array.networks) + } + private val tcpTypes: Array by lazy { + resources.getStringArray(R.array.header_type_tcp) + } + private val kcpAndQuicTypes: Array by lazy { + resources.getStringArray(R.array.header_type_kcp_and_quic) + } + private val grpcModes: Array by lazy { + resources.getStringArray(R.array.mode_type_grpc) + } + private val streamSecuritys: Array by lazy { + resources.getStringArray(R.array.streamsecurityxs) + } + private val allowinsecures: Array by lazy { + resources.getStringArray(R.array.allowinsecures) + } + private val uTlsItems: Array by lazy { + resources.getStringArray(R.array.streamsecurity_utls) + } + private val alpns: Array by lazy { + resources.getStringArray(R.array.streamsecurity_alpn) + } + private val xhttpMode: Array 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 + // protocols. Use findViewById manually ensures the xml are de-coupled with the activity logic. + private val et_remarks: EditText by lazy { findViewById(R.id.et_remarks) } + private val et_address: EditText by lazy { findViewById(R.id.et_address) } + private val et_port: EditText by lazy { findViewById(R.id.et_port) } + private val et_id: EditText by lazy { findViewById(R.id.et_id) } + private val et_security: EditText? by lazy { findViewById(R.id.et_security) } + private val sp_flow: Spinner? by lazy { findViewById(R.id.sp_flow) } + private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) } + private val sp_stream_security: Spinner? by lazy { findViewById(R.id.sp_stream_security) } + private val sp_allow_insecure: Spinner? by lazy { findViewById(R.id.sp_allow_insecure) } + private val container_allow_insecure: LinearLayout? by lazy { findViewById(R.id.lay_allow_insecure) } + private val et_sni: EditText? by lazy { findViewById(R.id.et_sni) } + private val container_sni: LinearLayout? by lazy { findViewById(R.id.lay_sni) } + private val sp_stream_fingerprint: Spinner? by lazy { findViewById(R.id.sp_stream_fingerprint) } //uTLS + private val container_fingerprint: LinearLayout? by lazy { findViewById(R.id.lay_stream_fingerprint) } + private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) } + private val sp_header_type: Spinner? by lazy { findViewById(R.id.sp_header_type) } + private val sp_header_type_title: TextView? by lazy { findViewById(R.id.sp_header_type_title) } + private val tv_request_host: TextView? by lazy { findViewById(R.id.tv_request_host) } + private val et_request_host: EditText? by lazy { findViewById(R.id.et_request_host) } + private val tv_path: TextView? by lazy { findViewById(R.id.tv_path) } + private val et_path: EditText? by lazy { findViewById(R.id.et_path) } + 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) } + private val et_spider_x: EditText? by lazy { findViewById(R.id.et_spider_x) } + private val container_spider_x: LinearLayout? by lazy { findViewById(R.id.lay_spider_x) } + private val et_reserved1: EditText? by lazy { findViewById(R.id.et_reserved1) } + private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) } + private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) } + private val et_obfs_password: EditText? by lazy { findViewById(R.id.et_obfs_password) } + 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_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) } + private val et_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) } + private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) } + private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) } + + private val et_keepalive: EditText? by lazy { findViewById(R.id.et_keepalive) } + private val et_wnoise: EditText? by lazy { findViewById(R.id.et_wnoise) } + private val et_wnoisecount: EditText? by lazy { findViewById(R.id.et_wnoisecount) } + private val et_wnoisedelay: EditText? by lazy { findViewById(R.id.et_wnoisedelay) } + private val et_wpayloadsize: EditText? by lazy { findViewById(R.id.et_wpayloadsize) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + title = getString(R.string.title_server) + + val config = MmkvManager.decodeServerConfig(editGuid) + when (config?.configType ?: createConfigType) { + EConfigType.VMESS -> setContentView(R.layout.activity_server_vmess) + EConfigType.CUSTOM -> return + EConfigType.SHADOWSOCKS -> setContentView(R.layout.activity_server_shadowsocks) + EConfigType.SOCKS -> setContentView(R.layout.activity_server_socks) + EConfigType.HTTP -> setContentView(R.layout.activity_server_socks) + EConfigType.VLESS -> setContentView(R.layout.activity_server_vless) + EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan) + EConfigType.WIREGUARD -> setContentView(R.layout.activity_server_wireguard) + EConfigType.HYSTERIA2 -> setContentView(R.layout.activity_server_hysteria2) + } + sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long, + ) { + val types = transportTypes(networks[position]) + sp_header_type?.isEnabled = types.size > 1 + val adapter = + 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 = + when (networks[position]) { + NetworkType.GRPC.type -> getString(R.string.server_lab_mode_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.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]) { + 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.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 + } + ) + ) + + tv_path?.text = Utils.getEditable( + getString( + when (networks[position]) { + 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.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.XHTTP.type -> config?.xhttpExtra + else -> null + }.orEmpty() + ) + + layout_extra?.visibility = + when (networks[position]) { + NetworkType.XHTTP.type -> View.VISIBLE + else -> View.GONE + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + // do nothing + } + } + sp_stream_security?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long, + ) { + val isBlank = streamSecuritys[position].isBlank() + val isTLS = streamSecuritys[position] == TLS + + when { + // Case 1: Null or blank + isBlank -> { + listOf( + container_sni, container_fingerprint, container_alpn, + container_allow_insecure, container_public_key, + container_short_id, container_spider_x + ).forEach { it?.visibility = View.GONE } + } + + // Case 2: TLS value + isTLS -> { + listOf( + container_sni, + container_fingerprint, + container_alpn + ).forEach { it?.visibility = View.VISIBLE } + container_allow_insecure?.visibility = View.VISIBLE + listOf( + container_public_key, + container_short_id, + container_spider_x + ).forEach { it?.visibility = View.GONE } + } + + // Case 3: Other reality values + else -> { + listOf(container_sni, container_fingerprint).forEach { + it?.visibility = View.VISIBLE + } + container_alpn?.visibility = View.GONE + container_allow_insecure?.visibility = View.GONE + listOf( + container_public_key, + container_short_id, + container_spider_x + ).forEach { it?.visibility = View.VISIBLE } + } + } + } + + override fun onNothingSelected(p0: AdapterView<*>?) { + // do nothing + } + } + if (config != null) { + bindingServer(config) + } else { + clearServer() + } + } + + /** + * binding selected server config + */ + private fun bindingServer(config: ProfileItem): Boolean { + + et_remarks.text = Utils.getEditable(config.remarks) + et_address.text = Utils.getEditable(config.server.orEmpty()) + et_port.text = Utils.getEditable(config.serverPort ?: DEFAULT_PORT.toString()) + et_id.text = Utils.getEditable(config.password.orEmpty()) + + if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) { + et_security?.text = Utils.getEditable(config.username.orEmpty()) + } else if (config.configType == EConfigType.VLESS) { + et_security?.text = Utils.getEditable(config.method.orEmpty()) + val flow = Utils.arrayFind(flows, config.flow.orEmpty()) + if (flow >= 0) { + sp_flow?.setSelection(flow) + } + } else if (config.configType == EConfigType.WIREGUARD) { + et_id.text = Utils.getEditable(config.secretKey.orEmpty()) + et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty()) + 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) + + et_keepalive?.text = Utils.getEditable(config.keepAlive?.toString() ?: WIREGUARD_keep_alive) + + et_wnoise?.text = Utils.getEditable(config.wnoise.orEmpty()) + et_wnoisecount?.text = Utils.getEditable(config.wnoisecount.orEmpty()) + et_wnoisedelay?.text = Utils.getEditable(config.wnoisedelay.orEmpty()) + et_wpayloadsize?.text = Utils.getEditable(config.wpayloadsize.orEmpty()) + + + } 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) + et_bandwidth_down?.text = Utils.getEditable(config.bandwidthDown) + et_bandwidth_up?.text = Utils.getEditable(config.bandwidthUp) + } + 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) + } + + val streamSecurity = Utils.arrayFind(streamSecuritys, config.security.orEmpty()) + if (streamSecurity >= 0) { + sp_stream_security?.setSelection(streamSecurity) + container_sni?.visibility = View.VISIBLE + container_fingerprint?.visibility = View.VISIBLE + container_alpn?.visibility = View.VISIBLE + + et_sni?.text = Utils.getEditable(config.sni) + config.fingerPrint?.let { + val utlsIndex = Utils.arrayFind(uTlsItems, it) + sp_stream_fingerprint?.setSelection(utlsIndex) + } + config.alpn?.let { + val alpnIndex = Utils.arrayFind(alpns, it) + sp_stream_alpn?.setSelection(alpnIndex) + } + if (config.security == TLS) { + container_allow_insecure?.visibility = View.VISIBLE + val allowinsecure = Utils.arrayFind(allowinsecures, config.insecure.toString()) + if (allowinsecure >= 0) { + sp_allow_insecure?.setSelection(allowinsecure) + } + container_public_key?.visibility = View.GONE + container_short_id?.visibility = View.GONE + container_spider_x?.visibility = View.GONE + } 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 + et_short_id?.text = Utils.getEditable(config.shortId.orEmpty()) + container_spider_x?.visibility = View.VISIBLE + et_spider_x?.text = Utils.getEditable(config.spiderX.orEmpty()) + container_allow_insecure?.visibility = View.GONE + } + } + + if (config.security.isNullOrEmpty()) { + container_sni?.visibility = View.GONE + container_fingerprint?.visibility = View.GONE + container_alpn?.visibility = View.GONE + container_allow_insecure?.visibility = View.GONE + container_public_key?.visibility = View.GONE + 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) + } + return true + } + + /** + * clear or init server config + */ + private fun clearServer(): Boolean { + et_remarks.text = null + et_address.text = null + et_port.text = Utils.getEditable(DEFAULT_PORT.toString()) + et_id.text = null + sp_security?.setSelection(0) + sp_network?.setSelection(0) + + sp_header_type?.setSelection(0) + et_request_host?.text = null + et_path?.text = null + sp_stream_security?.setSelection(0) + sp_allow_insecure?.setSelection(0) + et_sni?.text = null + + //et_security.text = null + sp_flow?.setSelection(0) + et_public_key?.text = null + et_reserved1?.text = Utils.getEditable("0,0,0") + et_local_address?.text = + Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}") + et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU) + return true + } + + /** + * save server config + */ + private fun saveServer(): Boolean { + if (TextUtils.isEmpty(et_remarks.text.toString())) { + toast(R.string.server_lab_remarks) + return false + } + if (TextUtils.isEmpty(et_address.text.toString())) { + toast(R.string.server_lab_address) + return false + } + if (createConfigType != EConfigType.HYSTERIA2) { + if (Utils.parseInt(et_port.text.toString()) <= 0) { + toast(R.string.server_lab_port) + return false + } + } + val config = + MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(createConfigType) + if (config.configType != EConfigType.SOCKS + && config.configType != EConfigType.HTTP + && TextUtils.isEmpty(et_id.text.toString()) + ) { + if (config.configType == EConfigType.TROJAN + || config.configType == EConfigType.SHADOWSOCKS + || config.configType == EConfigType.HYSTERIA2 + ) { + toast(R.string.server_lab_id3) + } else { + toast(R.string.server_lab_id) + } + return false + } + sp_stream_security?.let { + if (config.configType == EConfigType.TROJAN && TextUtils.isEmpty(streamSecuritys[it.selectedItemPosition])) { + toast(R.string.server_lab_stream_security) + 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) + saveTls(config) + + if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) { + config.subscriptionId = subscriptionId.orEmpty() + } + Log.d(ANG_PACKAGE, JsonUtil.toJsonPretty(config) ?: "") + MmkvManager.encodeServerConfig(editGuid, config) + toast(R.string.toast_success) + finish() + return true + } + + private fun saveCommon(config: ProfileItem) { + config.remarks = et_remarks.text.toString().trim() + config.server = et_address.text.toString().trim() + config.serverPort = et_port.text.toString().trim() + config.password = et_id.text.toString().trim() + + if (config.configType == EConfigType.VMESS) { + config.method = securitys[sp_security?.selectedItemPosition ?: 0] + } else if (config.configType == EConfigType.VLESS) { + config.method = et_security?.text.toString().trim() + config.flow = flows[sp_flow?.selectedItemPosition ?: 0] + } else if (config.configType == EConfigType.SHADOWSOCKS) { + config.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0] + } else if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) { + if (!TextUtils.isEmpty(et_security?.text) || !TextUtils.isEmpty(et_id.text)) { + config.username = et_security?.text.toString().trim() + } + } else if (config.configType == EConfigType.TROJAN) { + } 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()) + + config.keepAlive = Utils.parseInt(et_keepalive?.text.toString()) + config.wnoise = et_wnoise?.text.toString().trim() + config.wnoisecount = et_wnoisecount?.text.toString().trim() + config.wnoisedelay = et_wnoisedelay?.text.toString().trim() + config.wpayloadsize = et_wpayloadsize?.text.toString().trim() + + } else if (config.configType == EConfigType.HYSTERIA2) { + config.obfsPassword = et_obfs_password?.text?.toString() + config.portHopping = et_port_hop?.text?.toString() + config.portHoppingInterval = et_port_hop_interval?.text?.toString() + config.pinSHA256 = et_pinsha256?.text?.toString() + config.bandwidthDown = et_bandwidth_down?.text?.toString() + config.bandwidthUp = et_bandwidth_up?.text?.toString() + } + } + + + private fun saveStreamSettings(profileItem: ProfileItem) { + val network = sp_network?.selectedItemPosition ?: return + val type = sp_header_type?.selectedItemPosition ?: return + val requestHost = et_request_host?.text?.toString()?.trim() ?: return + val path = et_path?.text?.toString()?.trim() ?: return + + profileItem.network = networks[network] + profileItem.headerType = transportTypes(networks[network])[type] + profileItem.host = requestHost + profileItem.path = path + profileItem.seed = path + profileItem.quicSecurity = requestHost + profileItem.quicKey = path + 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) { + val streamSecurity = sp_stream_security?.selectedItemPosition ?: return + val sniField = et_sni?.text?.toString()?.trim() + val allowInsecureField = sp_allow_insecure?.selectedItemPosition + val utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: 0 + val alpnIndex = sp_stream_alpn?.selectedItemPosition ?: 0 + val publicKey = et_public_key?.text?.toString() + val shortId = et_short_id?.text?.toString() + val spiderX = et_spider_x?.text?.toString() + + val allowInsecure = + if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) { + MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE) + } else { + allowinsecures[allowInsecureField].toBoolean() + } + + config.security = streamSecuritys[streamSecurity] + config.insecure = allowInsecure + config.sni = sniField + config.fingerPrint = uTlsItems[utlsIndex] + config.alpn = alpns[alpnIndex] + config.publicKey = publicKey + config.shortId = shortId + config.spiderX = spiderX + } + + private fun transportTypes(network: String?): Array { + return when (network) { + NetworkType.TCP.type -> { + tcpTypes + } + + NetworkType.KCP.type -> { + kcpAndQuicTypes + } + + "quic" -> { + kcpAndQuicTypes + } + + NetworkType.GRPC.type -> { + grpcModes + } + + NetworkType.XHTTP.type -> { + xhttpMode + } + + else -> { + arrayOf("---") + } + } + } + + /** + * delete server config + */ + private fun deleteServer(): Boolean { + if (editGuid.isNotEmpty()) { + if (editGuid != MmkvManager.getSelectServer()) { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { + AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + MmkvManager.removeServer(editGuid) + finish() + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + // do nothing + } + .show() + } else { + MmkvManager.removeServer(editGuid) + finish() + } + } else { + application.toast(R.string.toast_action_not_allowed) + } + } + return true + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.action_server, menu) + val delButton = menu.findItem(R.id.del_config) + val saveButton = menu.findItem(R.id.save_config) + + if (editGuid.isNotEmpty()) { + if (isRunning) { + delButton?.isVisible = false + saveButton?.isVisible = false + } + } else { + delButton?.isVisible = false + } + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.del_config -> { + deleteServer() + true + } + + R.id.save_config -> { + saveServer() + true + } + + else -> super.onOptionsItemSelected(item) + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerCustomConfigActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt similarity index 73% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerCustomConfigActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt index 3df4d3a040..6dba8faffb 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerCustomConfigActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt @@ -8,28 +8,24 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import com.blacksquircle.ui.editorkit.utils.EditorTheme import com.blacksquircle.ui.language.json.JsonLanguage -import com.google.gson.Gson -import com.tencent.mmkv.MMKV import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ServerConfig -import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.fmt.CustomFmt +import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.util.Utils import me.drakeet.support.toast.ToastCompat class ServerCustomConfigActivity : BaseActivity() { private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) } - private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) } - private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) } private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() } private val isRunning by lazy { intent.getBooleanExtra("isRunning", false) && editGuid.isNotEmpty() - && editGuid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) + && editGuid == MmkvManager.getSelectServer() } override fun onCreate(savedInstanceState: Bundle?) { @@ -50,16 +46,14 @@ class ServerCustomConfigActivity : BaseActivity() { } /** - * bingding seleced server config + * Binding selected server config */ - private fun bindingServer(config: ServerConfig): Boolean { + private fun bindingServer(config: ProfileItem): Boolean { binding.etRemarks.text = Utils.getEditable(config.remarks) - val raw = serverRawStorage?.decodeString(editGuid) - if (raw.isNullOrBlank()) { - binding.editor.setTextContent(Utils.getEditable(config.fullConfig?.toPrettyPrinting().orEmpty())) - } else { - binding.editor.setTextContent(Utils.getEditable(raw)) - } + val raw = MmkvManager.decodeServerRaw(editGuid) + val configContent = raw.orEmpty() + + binding.editor.setTextContent(Utils.getEditable(configContent)) return true } @@ -80,20 +74,23 @@ class ServerCustomConfigActivity : BaseActivity() { return false } - val v2rayConfig = try { - Gson().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() return false } - val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM) - config.remarks = if (binding.etRemarks.text.isNullOrEmpty()) v2rayConfig.remarks.orEmpty() else binding.etRemarks.text.toString() - config.fullConfig = v2rayConfig + val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(EConfigType.CUSTOM) + 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) - serverRawStorage?.encode(editGuid, binding.editor.text.toString()) + MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString()) toast(R.string.toast_success) finish() return true @@ -109,7 +106,7 @@ class ServerCustomConfigActivity : BaseActivity() { MmkvManager.removeServer(editGuid) finish() } - .setNegativeButton(android.R.string.no) { _, _ -> + .setNegativeButton(android.R.string.cancel) { _, _ -> // do nothing } .show() diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt similarity index 72% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt index b91fd043b8..f73ae6fb03 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt @@ -8,17 +8,17 @@ import androidx.activity.viewModels import androidx.preference.CheckBoxPreference import androidx.preference.EditTextPreference import androidx.preference.ListPreference -import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequest import androidx.work.multiprocess.RemoteWorkManager -import com.tencent.mmkv.MMKV import com.v2ray.ang.AngApplication import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.VPN import com.v2ray.ang.R +import com.v2ray.ang.extension.toLongEx +import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.service.SubscriptionUpdater -import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.Utils import com.v2ray.ang.viewmodel.SettingsViewModel import java.util.concurrent.TimeUnit @@ -36,15 +36,14 @@ class SettingsActivity : BaseActivity() { } class SettingsFragment : PreferenceFragmentCompat() { - private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } private val perAppProxy by lazy { findPreference(AppConfig.PREF_PER_APP_PROXY) } private val localDns by lazy { findPreference(AppConfig.PREF_LOCAL_DNS_ENABLED) } private val fakeDns by lazy { findPreference(AppConfig.PREF_FAKE_DNS_ENABLED) } + private val appendHttpProxy by lazy { findPreference(AppConfig.PREF_APPEND_HTTP_PROXY) } private val localDnsPort by lazy { findPreference(AppConfig.PREF_LOCAL_DNS_PORT) } private val vpnDns by lazy { findPreference(AppConfig.PREF_VPN_DNS) } - - private val routingCustom by lazy { findPreference(AppConfig.PREF_ROUTING_CUSTOM) } + private val vpnBypassLan by lazy { findPreference(AppConfig.PREF_VPN_BYPASS_LAN) } private val mux by lazy { findPreference(AppConfig.PREF_MUX_ENABLED) } private val muxConcurrency by lazy { findPreference(AppConfig.PREF_MUX_CONCURRENCY) } @@ -60,9 +59,9 @@ class SettingsActivity : BaseActivity() { private val autoUpdateInterval by lazy { findPreference(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) } private val socksPort by lazy { findPreference(AppConfig.PREF_SOCKS_PORT) } - private val httpPort by lazy { findPreference(AppConfig.PREF_HTTP_PORT) } private val remoteDns by lazy { findPreference(AppConfig.PREF_REMOTE_DNS) } private val domesticDns by lazy { findPreference(AppConfig.PREF_DOMESTIC_DNS) } + private val dnsHosts by lazy { findPreference(AppConfig.PREF_DNS_HOSTS) } private val delayTestUrl by lazy { findPreference(AppConfig.PREF_DELAY_TEST_URL) } private val mode by lazy { findPreference(AppConfig.PREF_MODE) } @@ -89,11 +88,6 @@ class SettingsActivity : BaseActivity() { true } - routingCustom?.setOnPreferenceClickListener { - startActivity(Intent(activity, RoutingSettingsActivity::class.java)) - false - } - mux?.setOnPreferenceChangeListener { _, newValue -> updateMux(newValue as Boolean) true @@ -128,7 +122,7 @@ class SettingsActivity : BaseActivity() { val value = newValue as Boolean autoUpdateCheck?.isChecked = value autoUpdateInterval?.isEnabled = value - autoUpdateInterval?.text?.toLong()?.let { + autoUpdateInterval?.text?.toLongEx()?.let { if (newValue) configureUpdateTask(it) else cancelUpdateTask() } true @@ -138,9 +132,9 @@ class SettingsActivity : BaseActivity() { // It must be greater than 15 minutes because WorkManager couldn't run tasks under 15 minutes intervals nval = - if (TextUtils.isEmpty(nval) || nval.toLong() < 15) AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL else nval + if (TextUtils.isEmpty(nval) || nval.toLongEx() < 15) AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL else nval autoUpdateInterval?.summary = nval - configureUpdateTask(nval.toLong()) + configureUpdateTask(nval.toLongEx()) true } @@ -149,11 +143,7 @@ class SettingsActivity : BaseActivity() { socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval true } - httpPort?.setOnPreferenceChangeListener { _, any -> - val nval = any as String - httpPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_HTTP else nval - true - } + remoteDns?.setOnPreferenceChangeListener { _, any -> val nval = any as String remoteDns?.summary = if (nval == "") AppConfig.DNS_PROXY else nval @@ -164,6 +154,11 @@ class SettingsActivity : BaseActivity() { domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval true } + dnsHosts?.setOnPreferenceChangeListener { _, any -> + val nval = any as String + dnsHosts?.summary = nval + true + } delayTestUrl?.setOnPreferenceChangeListener { _, any -> val nval = any as String delayTestUrl?.summary = if (nval == "") AppConfig.DelayTestUrl else nval @@ -180,33 +175,34 @@ class SettingsActivity : BaseActivity() { override fun onStart() { super.onStart() - updateMode(settingsStorage.decodeString(AppConfig.PREF_MODE, "VPN")) - localDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false) - fakeDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FAKE_DNS_ENABLED, false) - localDnsPort?.summary = settingsStorage.decodeString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS) - vpnDns?.summary = settingsStorage.decodeString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN) - - updateMux(settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false)) - mux?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false) - muxConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_CONCURRENCY, "8") - muxXudpConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8") - - updateFragment(settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false)) - fragment?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false) - fragmentPackets?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello") - fragmentLength?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_LENGTH, "10-20") - fragmentInterval?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20") - - autoUpdateCheck?.isChecked = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) + updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN)) + localDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED, false) + fakeDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED, false) + appendHttpProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY, false) + localDnsPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS) + vpnDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN) + + updateMux(MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)) + mux?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false) + muxConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8") + muxXudpConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8") + + updateFragment(MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false)) + fragment?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) + fragmentPackets?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello") + fragmentLength?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "10-20") + fragmentInterval?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20") + + autoUpdateCheck?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) autoUpdateInterval?.summary = - settingsStorage.decodeString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL) - autoUpdateInterval?.isEnabled = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) + MmkvManager.decodeSettingsString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL) + autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) - socksPort?.summary = settingsStorage.decodeString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS) - httpPort?.summary = settingsStorage.decodeString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP) - remoteDns?.summary = settingsStorage.decodeString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY) - domesticDns?.summary = settingsStorage.decodeString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT) - delayTestUrl?.summary = settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl) + socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS) + remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY) + domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT) + dnsHosts?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS) + delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl) initSharedPreference() } @@ -221,7 +217,6 @@ class SettingsActivity : BaseActivity() { fragmentInterval, autoUpdateInterval, socksPort, - httpPort, remoteDns, domesticDns, delayTestUrl @@ -233,11 +228,12 @@ class SettingsActivity : BaseActivity() { AppConfig.PREF_SNIFFING_ENABLED, ).forEach { key -> findPreference(key)?.isChecked = - settingsStorage.decodeBool(key, true) + MmkvManager.decodeSettingsBool(key, true) } listOf( AppConfig.PREF_ROUTE_ONLY_ENABLED, + AppConfig.PREF_IS_BOOTED, AppConfig.PREF_BYPASS_APPS, AppConfig.PREF_SPEED_ENABLED, AppConfig.PREF_CONFIRM_REMOVE, @@ -247,12 +243,12 @@ class SettingsActivity : BaseActivity() { AppConfig.PREF_ALLOW_INSECURE ).forEach { key -> findPreference(key)?.isChecked = - settingsStorage.decodeBool(key, false) + MmkvManager.decodeSettingsBool(key, false) } listOf( + AppConfig.PREF_VPN_BYPASS_LAN, AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, - AppConfig.PREF_ROUTING_MODE, AppConfig.PREF_MUX_XUDP_QUIC, AppConfig.PREF_FRAGMENT_PACKETS, AppConfig.PREF_LANGUAGE, @@ -260,23 +256,26 @@ class SettingsActivity : BaseActivity() { AppConfig.PREF_LOGLEVEL, AppConfig.PREF_MODE ).forEach { key -> - if (settingsStorage.decodeString(key) != null) { - findPreference(key)?.value = settingsStorage.decodeString(key) + if (MmkvManager.decodeSettingsString(key) != null) { + findPreference(key)?.value = MmkvManager.decodeSettingsString(key) } } } private fun updateMode(mode: String?) { - val vpn = mode == "VPN" + val vpn = mode == VPN perAppProxy?.isEnabled = vpn - perAppProxy?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false) + perAppProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false) localDns?.isEnabled = vpn fakeDns?.isEnabled = vpn + appendHttpProxy?.isEnabled = vpn localDnsPort?.isEnabled = vpn vpnDns?.isEnabled = vpn + vpnBypassLan?.isEnabled = vpn + vpn if (vpn) { updateLocalDns( - settingsStorage.getBoolean( + MmkvManager.decodeSettingsBool( AppConfig.PREF_LOCAL_DNS_ENABLED, false ) @@ -318,19 +317,17 @@ class SettingsActivity : BaseActivity() { muxXudpConcurrency?.isEnabled = enabled muxXudpQuic?.isEnabled = enabled if (enabled) { - updateMuxConcurrency(settingsStorage.decodeString(AppConfig.PREF_MUX_CONCURRENCY, "8")) - updateMuxXudpConcurrency(settingsStorage.decodeString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")) + updateMuxConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8")) + updateMuxXudpConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")) } } private fun updateMuxConcurrency(value: String?) { - if (value == null) { - } else { - val concurrency = value.toIntOrNull() ?: 8 - muxConcurrency?.summary = concurrency.toString() - } + val concurrency = value?.toIntOrNull() ?: 8 + muxConcurrency?.summary = concurrency.toString() } + private fun updateMuxXudpConcurrency(value: String?) { if (value == null) { muxXudpQuic?.isEnabled = true @@ -346,9 +343,9 @@ class SettingsActivity : BaseActivity() { fragmentLength?.isEnabled = enabled fragmentInterval?.isEnabled = enabled if (enabled) { - updateFragmentPackets(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")) - updateFragmentLength(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_LENGTH, "10-20")) - updateFragmentInterval(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")) + updateFragmentPackets(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")) + updateFragmentLength(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "10-20")) + updateFragmentInterval(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")) } } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt similarity index 73% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt index d9546f34b2..45f92c2df0 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt @@ -6,13 +6,11 @@ import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AlertDialog import androidx.lifecycle.lifecycleScope -import com.google.gson.Gson -import com.tencent.mmkv.MMKV import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivitySubEditBinding import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.util.Utils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -23,7 +21,6 @@ class SubEditActivity : BaseActivity() { var del_config: MenuItem? = null var save_config: MenuItem? = null - private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) } private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() } override fun onCreate(savedInstanceState: Bundle?) { @@ -31,9 +28,9 @@ class SubEditActivity : BaseActivity() { setContentView(binding.root) title = getString(R.string.title_sub_setting) - val json = subStorage?.decodeString(editSubId) - if (!json.isNullOrBlank()) { - bindingServer(Gson().fromJson(json, SubscriptionItem::class.java)) + val subItem = MmkvManager.decodeSubscription(editSubId) + if (subItem != null) { + bindingServer(subItem) } else { clearServer() } @@ -45,8 +42,11 @@ class SubEditActivity : BaseActivity() { private fun bindingServer(subItem: SubscriptionItem): Boolean { binding.etRemarks.text = Utils.getEditable(subItem.remarks) binding.etUrl.text = Utils.getEditable(subItem.url) + binding.etFilter.text = Utils.getEditable(subItem.filter) binding.chkEnable.isChecked = subItem.enabled binding.autoUpdateCheck.isChecked = subItem.autoUpdate + binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile) + binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile) return true } @@ -56,7 +56,10 @@ class SubEditActivity : BaseActivity() { private fun clearServer(): Boolean { binding.etRemarks.text = null binding.etUrl.text = null + binding.etFilter.text = null binding.chkEnable.isChecked = true + binding.etPreProfile.text = null + binding.etNextProfile.text = null return true } @@ -64,31 +67,33 @@ class SubEditActivity : BaseActivity() { * save server config */ private fun saveServer(): Boolean { - val subItem: SubscriptionItem - val json = subStorage?.decodeString(editSubId) - var subId = editSubId - if (!json.isNullOrBlank()) { - subItem = Gson().fromJson(json, SubscriptionItem::class.java) - } else { - subId = Utils.getUuid() - subItem = SubscriptionItem() - } + val subItem = MmkvManager.decodeSubscription(editSubId) ?: SubscriptionItem() subItem.remarks = binding.etRemarks.text.toString() subItem.url = binding.etUrl.text.toString() + subItem.filter = binding.etFilter.text.toString() subItem.enabled = binding.chkEnable.isChecked subItem.autoUpdate = binding.autoUpdateCheck.isChecked + subItem.prevProfile = binding.etPreProfile.text.toString() + subItem.nextProfile = binding.etNextProfile.text.toString() if (TextUtils.isEmpty(subItem.remarks)) { toast(R.string.sub_setting_remarks) return false } -// if (TextUtils.isEmpty(subItem.url)) { -// toast(R.string.sub_setting_url) -// return false -// } + if (subItem.url.isNotEmpty()) { + if (!Utils.isValidUrl(subItem.url)) { + toast(R.string.toast_invalid_url) + return false + } + + if (!Utils.isValidSubUrl(subItem.url)) { + toast(R.string.toast_insecure_url_protocol) + //return false + } + } - subStorage?.encode(subId, Gson().toJson(subItem)) + MmkvManager.encodeSubscription(editSubId, subItem) toast(R.string.toast_success) finish() return true @@ -108,7 +113,7 @@ class SubEditActivity : BaseActivity() { } } } - .setNegativeButton(android.R.string.no) { _, _ -> + .setNegativeButton(android.R.string.cancel) { _, _ -> // do nothing } .show() diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt similarity index 84% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt index 29d9890069..e0ef627ce2 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt @@ -6,14 +6,16 @@ import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AlertDialog import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivitySubSettingBinding import com.v2ray.ang.databinding.LayoutProgressBinding import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.AngConfigManager -import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.handler.AngConfigManager +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.helper.SimpleItemTouchHelperCallback import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -23,6 +25,7 @@ class SubSettingActivity : BaseActivity() { var subscriptions: List> = listOf() private val adapter by lazy { SubSettingRecyclerAdapter(this) } + private var mItemTouchHelper: ItemTouchHelper? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -33,12 +36,14 @@ class SubSettingActivity : BaseActivity() { binding.recyclerView.setHasFixedSize(true) binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.adapter = adapter + + mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) + mItemTouchHelper?.attachToRecyclerView(binding.recyclerView) } override fun onResume() { super.onResume() - subscriptions = MmkvManager.decodeSubscriptions() - adapter.notifyDataSetChanged() + refreshData() } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -77,4 +82,9 @@ class SubSettingActivity : BaseActivity() { else -> super.onOptionsItemSelected(item) } + + fun refreshData() { + subscriptions = MmkvManager.decodeSubscriptions() + adapter.notifyDataSetChanged() + } } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt similarity index 71% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt index 9b3787cf8f..9f84c0935d 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt @@ -8,21 +8,20 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.RecyclerView -import com.google.gson.Gson -import com.tencent.mmkv.MMKV import com.v2ray.ang.R import com.v2ray.ang.databinding.ItemQrcodeBinding import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.helper.ItemTouchHelperAdapter +import com.v2ray.ang.helper.ItemTouchHelperViewHolder import com.v2ray.ang.util.QRCodeDecoder import com.v2ray.ang.util.Utils -class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : - RecyclerView.Adapter() { +class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter(), ItemTouchHelperAdapter { private var mActivity: SubSettingActivity = activity - private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) } private val share_method: Array by lazy { mActivity.resources.getStringArray(R.array.share_sub_method) @@ -35,11 +34,7 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : val subItem = mActivity.subscriptions[position].second holder.itemSubSettingBinding.tvName.text = subItem.remarks holder.itemSubSettingBinding.tvUrl.text = subItem.url - if (subItem.enabled) { - holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorAccent) - } else { - holder.itemSubSettingBinding.chkEnable.setBackgroundResource(0) - } + holder.itemSubSettingBinding.chkEnable.isChecked = subItem.enabled holder.itemView.setBackgroundColor(Color.TRANSPARENT) holder.itemSubSettingBinding.layoutEdit.setOnClickListener { @@ -48,10 +43,12 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : .putExtra("subId", subId) ) } - holder.itemSubSettingBinding.infoContainer.setOnClickListener { - subItem.enabled = !subItem.enabled - subStorage?.encode(subId, Gson().toJson(subItem)) - notifyItemChanged(position) + + holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked -> + if (!it.isPressed) return@setOnCheckedChangeListener + subItem.enabled = isChecked + MmkvManager.encodeSubscription(subId, subItem) + } if (TextUtils.isEmpty(subItem.url)) { @@ -99,5 +96,28 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : } class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) : - RecyclerView.ViewHolder(itemSubSettingBinding.root) + BaseViewHolder(itemSubSettingBinding.root), ItemTouchHelperViewHolder + + open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + fun onItemSelected() { + itemView.setBackgroundColor(Color.LTGRAY) + } + + fun onItemClear() { + itemView.setBackgroundColor(0) + } + } + + override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { + SettingsManager.swapSubscriptions(fromPosition, toPosition) + notifyItemMoved(fromPosition, toPosition) + return true + } + + override fun onItemMoveCompleted() { + mActivity.refreshData() + } + + override fun onItemDismiss(position: Int) { + } } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt similarity index 93% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt index edf352f7a6..16eac3a45e 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt @@ -8,11 +8,10 @@ import android.view.MenuItem import android.view.View import android.widget.ArrayAdapter import android.widget.ListView -import com.tencent.mmkv.MMKV import com.v2ray.ang.AppConfig import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityTaskerBinding -import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.handler.MmkvManager class TaskerActivity : BaseActivity() { private val binding by lazy { ActivityTaskerBinding.inflate(layoutInflater) } @@ -21,8 +20,6 @@ class TaskerActivity : BaseActivity() { private var lstData: ArrayList = ArrayList() private var lstGuid: ArrayList = ArrayList() - private val serverStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) @@ -31,7 +28,7 @@ class TaskerActivity : BaseActivity() { lstData.add("Default") lstGuid.add(AppConfig.TASKER_DEFAULT_GUID) - serverStorage?.allKeys()?.forEach { key -> + MmkvManager.decodeServerList()?.forEach { key -> MmkvManager.decodeServerConfig(key)?.let { config -> lstData.add(config.remarks) lstGuid.add(key) diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UrlSchemeActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt similarity index 98% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UrlSchemeActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt index 976ce6fbe8..fe16b2c590 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UrlSchemeActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt @@ -7,7 +7,7 @@ import android.util.Log import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityLogcatBinding import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.AngConfigManager +import com.v2ray.ang.handler.AngConfigManager import java.net.URLDecoder class UrlSchemeActivity : BaseActivity() { diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt similarity index 59% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt index 5a8f814e22..e926166bda 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt @@ -19,10 +19,9 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.google.gson.Gson import com.tbruyelle.rxpermissions3.RxPermissions -import com.tencent.mmkv.MMKV import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivitySubSettingBinding import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding @@ -30,10 +29,12 @@ import com.v2ray.ang.databinding.LayoutProgressBinding import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.extension.toTrafficString import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.util.Utils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File import java.io.FileOutputStream import java.net.HttpURLConnection @@ -45,11 +46,10 @@ import java.util.Date class UserAssetActivity : BaseActivity() { private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) } - private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } - private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) } val extDir by lazy { File(Utils.userAssetPath(this)) } val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat") + val builtInGeoFiles_c4u = arrayOf("geosite_c4u.dat", "geoip_c4u.dat") override fun onCreate(savedInstanceState: Bundle?) { @@ -72,23 +72,12 @@ class UserAssetActivity : BaseActivity() { return super.onCreateOptionsMenu(menu) } - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.add_file -> { - showFileChooser() - true - } - - R.id.add_url -> { - val intent = Intent(this, UserAssetUrlActivity::class.java) - startActivity(intent) - true - } - - R.id.download_file -> { - downloadGeoFiles() - true - } - + // Use when to streamline the option selection + 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) } @@ -121,31 +110,29 @@ class UserAssetActivity : BaseActivity() { } } - private val chooseFile = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { it -> - val uri = it.data?.data - if (it.resultCode == RESULT_OK && uri != null) { - val assetId = Utils.getUuid() - try { - val assetItem = AssetUrlItem( - getCursorName(uri) ?: uri.toString(), - "file" - ) + val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val uri = result.data?.data + if (result.resultCode == RESULT_OK && uri != null) { + val assetId = Utils.getUuid() + runCatching { + val assetItem = AssetUrlItem( + getCursorName(uri) ?: uri.toString(), + "file" + ) - // check remarks unique - val assetList = MmkvManager.decodeAssetUrls() - if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) { - toast(R.string.msg_remark_is_duplicate) - return@registerForActivityResult - } - assetStorage?.encode(assetId, Gson().toJson(assetItem)) + val assetList = MmkvManager.decodeAssetUrls() + if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) { + toast(R.string.msg_remark_is_duplicate) + } else { + MmkvManager.encodeAsset(assetId, assetItem) copyFile(uri) - } catch (e: Exception) { - toast(R.string.toast_asset_copy_failed) - MmkvManager.removeAssetUrl(assetId) } + }.onFailure { + toast(R.string.toast_asset_copy_failed) + MmkvManager.removeAssetUrl(assetId) } } + } private fun copyFile(uri: Uri): String { val targetFile = File(extDir, getCursorName(uri) ?: uri.toString()) @@ -171,34 +158,93 @@ 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) - .setCancelable(false) - .show() - toast(R.string.msg_downloading_content) +// val dialog = AlertDialog.Builder(this) +// .setView(LayoutProgressBinding.inflate(layoutInflater).root) +// .setCancelable(false) +// .show() +// toast(R.string.msg_downloading_content) - val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt()) + val httpPort = SettingsManager.getHttpPort() var assets = MmkvManager.decodeAssetUrls() assets = addBuiltInGeoItems(assets) assets.forEach { - //toast(getString(R.string.msg_downloading_content) + it) - lifecycleScope.launch(Dispatchers.IO) { - var result = downloadGeo(it.second, 60000, httpPort) - if (!result) { - result = downloadGeo(it.second, 60000, 0) - } - launch(Dispatchers.Main) { - if (result) { - toast(getString(R.string.toast_success) + " " + it.second.remarks) - binding.recyclerView.adapter?.notifyDataSetChanged() - } else { - toast(getString(R.string.toast_failure) + " " + it.second.remarks) + + try { + val builder = AlertDialog.Builder(this) + .setTitle("Update " + it.second.remarks + " ?") + .setMessage( + String.format( + "Download %s ?\n\nurl:\n%s", + it.second.remarks, + it.second.url + ) + ) + .setPositiveButton("Download") { dialog, id -> + toast(getString(R.string.msg_downloading_content)+" -> "+it.second.remarks) + lifecycleScope.launch(Dispatchers.IO) { + var result = downloadGeo(it.second, 60000, httpPort) + if (!result) { + result = downloadGeo(it.second, 60000, 0) + } + launch(Dispatchers.Main) { + if (result) { + toast(getString(R.string.toast_success) + " " + it.second.remarks) + binding.recyclerView.adapter?.notifyDataSetChanged() + } else { + toast(getString(R.string.toast_failure) + " " + it.second.remarks) + } +// dialog.dismiss() + } + } } - dialog.dismiss() - } + .setNegativeButton("Cancel") { dialog, id -> + + } + + builder.create().show() + + } catch (_:Exception){ + } + } } @@ -215,7 +261,7 @@ class UserAssetActivity : BaseActivity() { URL(item.url).openConnection( Proxy( Proxy.Type.HTTP, - InetSocketAddress("127.0.0.1", httpPort) + InetSocketAddress(LOOPBACK, httpPort) ) ) as HttpURLConnection } @@ -252,9 +298,28 @@ class UserAssetActivity : BaseActivity() { ) } + builtInGeoFiles_c4u + .filter { geoFile -> assets.none { it.second.remarks == geoFile } } + .forEach { + list.add( + Utils.getUuid() to AssetUrlItem( + it, + (AppConfig.GeoUrl_c4u + it).replace("_c4u","") + ) + ) + } return list + assets } + fun initAssets() { + lifecycleScope.launch(Dispatchers.Default) { + SettingsManager.initAssets(this@UserAssetActivity, assets) + withContext(Dispatchers.Main) { + binding.recyclerView.adapter?.notifyDataSetChanged() + } + } + } + inner class UserAssetAdapter : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder { return UserAssetViewHolder( @@ -286,10 +351,13 @@ class UserAssetActivity : BaseActivity() { if (item.second.remarks in builtInGeoFiles && item.second.url == AppConfig.GeoUrl + item.second.remarks) { holder.itemUserAssetBinding.layoutEdit.visibility = GONE - holder.itemUserAssetBinding.layoutRemove.visibility = GONE + //holder.itemUserAssetBinding.layoutRemove.visibility = GONE + }else if (item.second.remarks in builtInGeoFiles_c4u && item.second.url == AppConfig.GeoUrl_c4u + item.second.remarks.replace("_c4u","")) { + holder.itemUserAssetBinding.layoutEdit.visibility = GONE + //holder.itemUserAssetBinding.layoutRemove.visibility = GONE } else { holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE } - holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE + //holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE } holder.itemUserAssetBinding.layoutEdit.setOnClickListener { @@ -298,9 +366,16 @@ class UserAssetActivity : BaseActivity() { startActivity(intent) } holder.itemUserAssetBinding.layoutRemove.setOnClickListener { - file?.delete() - MmkvManager.removeAssetUrl(item.first) - binding.recyclerView.adapter?.notifyItemRemoved(position) + AlertDialog.Builder(this@UserAssetActivity).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + file?.delete() + MmkvManager.removeAssetUrl(item.first) + initAssets() + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + //do noting + } + .show() } } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetUrlActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt similarity index 81% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetUrlActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt index 63dba839a1..0f95dd7ad6 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetUrlActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt @@ -5,24 +5,26 @@ import android.text.TextUtils import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AlertDialog -import com.google.gson.Gson -import com.tencent.mmkv.MMKV import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.handler.MmkvManager 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 var save_config: MenuItem? = null val extDir by lazy { File(Utils.userAssetPath(this)) } - private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) } private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() } override fun onCreate(savedInstanceState: Bundle?) { @@ -30,11 +32,16 @@ class UserAssetUrlActivity : BaseActivity() { setContentView(binding.root) title = getString(R.string.title_user_asset_add_url) - val json = assetStorage?.decodeString(editAssetId) - if (!json.isNullOrBlank()) { - bindingAsset(Gson().fromJson(json, AssetUrlItem::class.java)) - } else { - clearAsset() + val assetItem = MmkvManager.decodeAsset(editAssetId) + 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() } } @@ -60,12 +67,9 @@ class UserAssetUrlActivity : BaseActivity() { * save asset config */ private fun saveServer(): Boolean { - val assetItem: AssetUrlItem - val json = assetStorage?.decodeString(editAssetId) + var assetItem = MmkvManager.decodeAsset(editAssetId) var assetId = editAssetId - if (!json.isNullOrBlank()) { - assetItem = Gson().fromJson(json, AssetUrlItem::class.java) - + if (assetItem != null) { // remove file associated with the asset val file = extDir.resolve(assetItem.remarks) if (file.exists()) { @@ -96,7 +100,7 @@ class UserAssetUrlActivity : BaseActivity() { return false } - assetStorage?.encode(assetId, Gson().toJson(assetItem)) + MmkvManager.encodeAsset(assetId, assetItem) toast(R.string.toast_success) finish() return true @@ -112,7 +116,7 @@ class UserAssetUrlActivity : BaseActivity() { MmkvManager.removeAssetUrl(editAssetId) finish() } - .setNegativeButton(android.R.string.no) { _, _ -> + .setNegativeButton(android.R.string.cancel) { _, _ -> // do nothing } .show() diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt similarity index 61% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt index 3e2a4d4a19..8ce94751ab 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt @@ -1,27 +1,23 @@ package com.v2ray.ang.util -import android.Manifest import android.content.Context import android.content.pm.ApplicationInfo -import android.content.pm.PackageInfo import android.content.pm.PackageManager import com.v2ray.ang.dto.AppInfo import io.reactivex.rxjava3.core.Observable object AppManagerUtil { - fun loadNetworkAppList(ctx: Context): ArrayList { + private fun loadNetworkAppList(ctx: Context): ArrayList { val packageManager = ctx.packageManager val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS) val apps = ArrayList() 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) @@ -35,9 +31,9 @@ object AppManagerUtil { it.onNext(loadNetworkAppList(ctx)) } - val PackageInfo.hasInternetPermission: Boolean - get() { - val permissions = requestedPermissions - return permissions?.any { it == Manifest.permission.INTERNET } ?: false - } +// val PackageInfo.hasInternetPermission: Boolean +// get() { +// val permissions = requestedPermissions +// return permissions?.any { it == Manifest.permission.INTERNET } ?: false +// } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/JsonUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/JsonUtil.kt new file mode 100644 index 0000000000..28e7e9566e --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/JsonUtil.kt @@ -0,0 +1,52 @@ +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 +import com.google.gson.reflect.TypeToken +import java.lang.reflect.Type + +object JsonUtil { + private var gson = Gson() + + fun toJson(src: Any?): String { + return gson.toJson(src) + } + + fun fromJson(src: String, cls: Class): T { + return gson.fromJson(src, cls) + } + + fun toJsonPretty(src: Any?): String? { + if (src == null) + return null + val gsonPre = GsonBuilder() + .setPrettyPrinting() + .disableHtmlEscaping() + .registerTypeAdapter( // custom serializer is needed here since JSON by default parse number as Double, core will fail to start + object : TypeToken() {}.type, + JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> + JsonPrimitive( + src?.toInt() + ) + } + ) + .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 + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MessageUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/MessageUtil.kt similarity index 100% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MessageUtil.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/util/MessageUtil.kt diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MyContextWrapper.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/MyContextWrapper.kt similarity index 100% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MyContextWrapper.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/util/MyContextWrapper.kt diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt new file mode 100644 index 0000000000..a7566076b3 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt @@ -0,0 +1,98 @@ +package com.v2ray.ang.util + +import android.content.Context +import android.os.SystemClock +import android.util.Log +import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.fmt.Hysteria2Fmt +import com.v2ray.ang.service.ProcessService +import java.io.File + +object PluginUtil { + //private const val HYSTERIA2 = "hysteria2-plugin" + private const val HYSTERIA2 = "libhysteria2.so" + private const val TAG = ANG_PACKAGE + private val procService: ProcessService by lazy { + ProcessService() + } + +// fun initPlugin(name: String): PluginManager.InitResult { +// return PluginManager.init(name)!! +// } + + fun runPlugin(context: Context, config: ProfileItem?, domainPort: String?) { + Log.d(TAG, "runPlugin") + + if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) { + val configFile = genConfigHy2(context, config, domainPort) ?: return + val cmd = genCmdHy2(context, configFile) + + procService.runProcess(context, cmd) + } + } + + fun stopPlugin() { + stopHy2() + } + + fun realPingHy2(context: Context, config: ProfileItem?): Long { + Log.d(TAG, "realPingHy2") + val retFailure = -1L + + if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) { + val socksPort = Utils.findFreePort(listOf(0)) + val configFile = genConfigHy2(context, config, "0:${socksPort}") ?: return retFailure + val cmd = genCmdHy2(context, configFile) + + val proc = ProcessService() + proc.runProcess(context, cmd) + Thread.sleep(1000L) + val delay = SpeedtestUtil.testConnection(context, socksPort) + proc.stopProcess() + + return delay.first + } + return retFailure + } + + private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? { + Log.d(TAG, "runPlugin $HYSTERIA2") + + val socksPort = domainPort?.split(":")?.last() + .let { if (it.isNullOrEmpty()) return null else it.toInt() } + val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null + + val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json") + Log.d(TAG, "runPlugin ${configFile.absolutePath}") + + configFile.parentFile?.mkdirs() + configFile.writeText(JsonUtil.toJson(hy2Config)) + Log.d(TAG, JsonUtil.toJson(hy2Config)) + + return configFile + } + + private fun genCmdHy2(context: Context, configFile: File): MutableList { + return mutableListOf( + File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath, + //initPlugin(HYSTERIA2).path, + "--disable-update-check", + "--config", + configFile.absolutePath, + "--log-level", + "warn", + "client" + ) + } + + private fun stopHy2() { + try { + Log.d(TAG, "$HYSTERIA2 destroy") + procService?.stopProcess() + } catch (e: Exception) { + Log.d(TAG, e.toString()) + } + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/QRCodeDecoder.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.kt similarity index 60% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/QRCodeDecoder.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.kt index fb0643da65..b8a1e27a06 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/QRCodeDecoder.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.kt @@ -23,36 +23,19 @@ object QRCodeDecoder { * create qrcode using zxing */ fun createQRCode(text: String, size: Int = 800): Bitmap? { - try { - val hints = HashMap() - hints[EncodeHintType.CHARACTER_SET] = "utf-8" - val bitMatrix = QRCodeWriter().encode( - text, - BarcodeFormat.QR_CODE, size, size, hints - ) - val pixels = IntArray(size * size) - for (y in 0 until size) { - for (x in 0 until size) { - if (bitMatrix.get(x, y)) { - pixels[y * size + x] = 0xff000000.toInt() - } else { - pixels[y * size + x] = 0xffffffff.toInt() - } - - } + return runCatching { + val hints = mapOf(EncodeHintType.CHARACTER_SET to Charsets.UTF_8.name()) + val bitMatrix = QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, size, size, hints) + val pixels = IntArray(size * size) { i -> + if (bitMatrix.get(i % size, i / size)) 0xff000000.toInt() else 0xffffffff.toInt() } - val bitmap = Bitmap.createBitmap( - size, size, - Bitmap.Config.ARGB_8888 - ) - bitmap.setPixels(pixels, 0, size, 0, 0, size, size) - return bitmap - } catch (e: Exception) { - e.printStackTrace() - return null - } + Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply { + setPixels(pixels, 0, size, 0, 0, size, size) + } + }.getOrNull() } + /** * 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。 * @@ -70,40 +53,24 @@ object QRCodeDecoder { * @return 返回二维码图片里的内容 或 null */ fun syncDecodeQRCode(bitmap: Bitmap?): String? { - if (bitmap == null) { - return null - } - var source: RGBLuminanceSource? = null - try { - val width = bitmap.width - val height = bitmap.height - val pixels = IntArray(width * height) - bitmap.getPixels(pixels, 0, width, 0, 0, width, height) - source = RGBLuminanceSource(width, height, pixels) - val qrReader = QRCodeReader() - try { - val result = try { - qrReader.decode( - BinaryBitmap(GlobalHistogramBinarizer(source)), - mapOf(DecodeHintType.TRY_HARDER to true) - ) + return bitmap?.let { + runCatching { + val pixels = IntArray(it.width * it.height).also { array -> + it.getPixels(array, 0, it.width, 0, 0, it.width, it.height) + } + val source = RGBLuminanceSource(it.width, it.height, pixels) + val qrReader = QRCodeReader() + + try { + qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source)), mapOf(DecodeHintType.TRY_HARDER to true)).text } catch (e: NotFoundException) { - qrReader.decode( - BinaryBitmap(GlobalHistogramBinarizer(source.invert())), - mapOf(DecodeHintType.TRY_HARDER to true) - ) + qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source.invert())), mapOf(DecodeHintType.TRY_HARDER to true)).text } - return result.text - } catch (e: Exception) { - e.printStackTrace() - } - } catch (e: Exception) { - e.printStackTrace() + }.getOrNull() } - - return null } + /** * 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR * @@ -149,6 +116,6 @@ object QRCodeDecoder { ) HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats - HINTS[DecodeHintType.CHARACTER_SET] = "utf-8" + HINTS[DecodeHintType.CHARACTER_SET] = Charsets.UTF_8.name() } } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/SpeedtestUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/SpeedtestUtil.kt similarity index 91% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/SpeedtestUtil.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/util/SpeedtestUtil.kt index 03a3f6ba7a..6a97dc604e 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/SpeedtestUtil.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/SpeedtestUtil.kt @@ -5,6 +5,7 @@ import android.os.SystemClock import android.text.TextUtils import android.util.Log import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.R import com.v2ray.ang.extension.responseLength import kotlinx.coroutines.isActive @@ -97,9 +98,9 @@ object SpeedtestUtil { } } - fun testConnection(context: Context, port: Int): String { - // return V2RayVpnService.measureV2rayDelay() + fun testConnection(context: Context, port: Int): Pair { var result: String + var elapsed = -1L var conn: HttpURLConnection? = null try { @@ -108,7 +109,7 @@ object SpeedtestUtil { conn = url.openConnection( Proxy( Proxy.Type.HTTP, - InetSocketAddress("127.0.0.1", port) + InetSocketAddress(LOOPBACK, port) ) ) as HttpURLConnection conn.connectTimeout = 30000 @@ -119,10 +120,10 @@ object SpeedtestUtil { val start = SystemClock.elapsedRealtime() val code = conn.responseCode - val elapsed = SystemClock.elapsedRealtime() - start + elapsed = SystemClock.elapsedRealtime() - start if (code == 204 || code == 200 && conn.responseLength == 0L) { - result = context.getString(R.string.connection_test_available, elapsed) + result = context.getString(R.string.connection_test_available, elapsed , elapsed) } else { throw IOException( context.getString( @@ -133,10 +134,7 @@ object SpeedtestUtil { } } catch (e: IOException) { // network exception - Log.d( - AppConfig.ANG_PACKAGE, - "testConnection IOException: " + Log.getStackTraceString(e) - ) + Log.d(AppConfig.ANG_PACKAGE, "testConnection IOException: " + Log.getStackTraceString(e)) result = context.getString(R.string.connection_test_error, e.message) } catch (e: Exception) { // library exception, eg sumsung @@ -146,7 +144,7 @@ object SpeedtestUtil { conn?.disconnect() } - return result + return Pair(elapsed, result) } fun getLibVersion(): String { diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt similarity index 70% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt index f8ad539082..320632ff38 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt @@ -17,22 +17,24 @@ import android.util.Log import android.util.Patterns import android.webkit.URLUtil import androidx.appcompat.app.AppCompatDelegate -import com.tencent.mmkv.MMKV +import androidx.core.content.ContextCompat 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 +import com.v2ray.ang.dto.Language import com.v2ray.ang.extension.toast +import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.service.V2RayServiceManager +import org.json.JSONArray +import org.json.JSONObject import java.io.IOException import java.net.* import java.util.* object Utils { - private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) } - private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } - /** * convert string to editalbe for kotlin * @@ -131,7 +133,8 @@ object Utils { * get remote dns servers from preference */ fun getRemoteDnsServers(): List { - val remoteDns = settingsStorage?.decodeString(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) @@ -140,7 +143,7 @@ object Utils { } fun getVpnDnsServers(): List { - val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN + val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN return vpnDns.split(",").filter { isPureIpAddress(it) } // allow empty, in that case dns will use system default } @@ -149,7 +152,8 @@ object Utils { * get remote dns servers from preference */ fun getDomesticDnsServers(): List { - val domesticDns = settingsStorage?.decodeString(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) @@ -160,8 +164,11 @@ object Utils { /** * is ip address */ - fun isIpAddress(value: String): Boolean { + fun isIpAddress(value: String?): Boolean { try { + if (value.isNullOrEmpty()) { + return false + } var addr = value if (addr.isEmpty() || addr.isBlank()) { return false @@ -246,7 +253,7 @@ object Utils { } fun startVServiceFromToggle(context: Context): Boolean { - if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) { + if (MmkvManager.getSelectServer().isNullOrEmpty()) { context.toast(R.string.app_tile_first_use) return false } @@ -281,7 +288,7 @@ object Utils { fun urlDecode(url: String): String { return try { - URLDecoder.decode(url, Charsets.UTF_8.toString()) + URLDecoder.decode(url, Charsets.UTF_8.name()) } catch (e: Exception) { e.printStackTrace() url @@ -290,7 +297,7 @@ object Utils { fun urlEncode(url: String): String { return try { - URLEncoder.encode(url, Charsets.UTF_8.toString()) + URLEncoder.encode(url, Charsets.UTF_8.name()).replace("+", "%20") } catch (e: Exception) { e.printStackTrace() url @@ -354,7 +361,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() @@ -362,7 +373,7 @@ object Utils { url.openConnection( Proxy( Proxy.Type.HTTP, - InetSocketAddress("127.0.0.1", httpPort) + InetSocketAddress(LOOPBACK, httpPort) ) ) } @@ -387,8 +398,8 @@ object Utils { } - fun setNightMode(context: Context) { - when (settingsStorage?.decodeString(AppConfig.PREF_UI_MODE_NIGHT, "0")) { + 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) "2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) @@ -407,17 +418,20 @@ object Utils { } fun getLocale(): Locale { - val lang = settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: "auto" - return when (lang) { - "auto" -> getSysLocale() - "en" -> Locale.ENGLISH - "zh-rCN" -> Locale.CHINA - "zh-rTW" -> Locale.TRADITIONAL_CHINESE - "vi" -> Locale("vi") - "ru" -> Locale("ru") - "fa" -> Locale("fa") - "bn" -> Locale("bn") - else -> getSysLocale() + val langCode = + MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code + val language = Language.fromCode(langCode) + + return when (language) { + Language.AUTO -> getSysLocale() + Language.ENGLISH -> Locale.ENGLISH + Language.CHINA -> Locale.CHINA + Language.TRADITIONAL_CHINESE -> Locale.TRADITIONAL_CHINESE + Language.VIETNAMESE -> Locale("vi") + Language.RUSSIAN -> Locale("ru") + Language.PERSIAN -> Locale("fa") + Language.BANGLA -> Locale("bn") + Language.BAKHTIARI -> Locale("bqi", "IR") } } @@ -451,15 +465,120 @@ object Utils { return if (second) { AppConfig.DelayTestUrl2 } else { - settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DelayTestUrl + MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) + ?: AppConfig.DelayTestUrl + } + } + + fun findFreePort(ports: List): Int { + for (port in ports) { + try { + return ServerSocket(port).use { it.localPort } + } catch (ex: IOException) { + continue // try next port + } + } + + // if the program gets here, no port in the range was found + throw IOException("no free port found") + } + + fun isValidSubUrl(value: String?): Boolean { + try { + if (value.isNullOrEmpty()) return false + if (URLUtil.isHttpsUrl(value)) return true + if (URLUtil.isHttpUrl(value) && value.contains(LOOPBACK)) return true + } catch (e: Exception) { + e.printStackTrace() } + return false + } + + fun receiverFlags(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.RECEIVER_EXPORTED + } else { + ContextCompat.RECEIVER_NOT_EXPORTED } + fun isXray(): Boolean = (ANG_PACKAGE.startsWith("com.v2ray.ang")) + + - fun removeKeepAlive(mystr: String): String { + //---------------- + private fun removeKeepAliveByRegex(mystr: String): String { val keepAliveRegex = "\"keepAlive\"\\s*:\\s*\\d+\\s*,?".toRegex(RegexOption.IGNORE_CASE) return keepAliveRegex.replace(mystr, "") } + + + fun removeKeepAliveAndRouting(content: String): String { + + // --------------- remove routing rules for lighter test -------- + try{ + val jt7 = JSONObject(content) + + try { + val empty_jarray = JSONArray() + jt7.getJSONObject("routing") + .put("rules", empty_jarray) + }catch (_: org.json.JSONException) { + } + + try { + val empty_jobj = JSONObject() + jt7.getJSONObject("dns") + .put("hosts", empty_jobj) + }catch (_: org.json.JSONException) { + } + + try { + val empty_jarr = JSONArray() + empty_jarr.put("1.1.1.1") + jt7.getJSONObject("dns") + .put("servers", empty_jarr) + }catch (_: org.json.JSONException) { + } + // --------------- end of routing rule deletion ------------------ + + if (content.contains("keepAlive" , ignoreCase = true)) { + + var protocol = "wireguard" + try{ + protocol = jt7.getJSONArray("outbounds") + .getJSONObject(0) + .getString("protocol") + +// println("config protocol:" + protocol) + }catch (e794: org.json.JSONException){ +// println("json wireguard ERR: " + e794.message) + } + + if(protocol.lowercase() == "wireguard") { + + jt7.getJSONArray("outbounds") + .getJSONObject(0) + .getJSONObject("settings") + .getJSONArray("peers") + .getJSONObject(0) + .remove("keepAlive") + +// println("keepAlive removed successfully") + } + + } + + val result = jt7.toString(2).replace("\\/", "/") + return result + }catch (e640: Exception) { +// println("Generic ERR when removing rule from TEST: $e640") + } + + return content + } + //-------------------------------------------- + + + } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/ZipUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/ZipUtil.kt similarity index 100% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/ZipUtil.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/util/ZipUtil.kt diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt similarity index 59% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt index dd28f84be9..74752a43fa 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt @@ -6,46 +6,38 @@ import android.content.Context import android.content.Intent 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 -import com.google.gson.Gson import com.v2ray.ang.AngApplication import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.R -import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.ProfileItem -import com.v2ray.ang.dto.ServerConfig import com.v2ray.ang.dto.ServersCache -import com.v2ray.ang.dto.SubscriptionItem -import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.extension.serializable import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.AngConfigManager -import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub +import com.v2ray.ang.fmt.CustomFmt +import com.v2ray.ang.handler.AngConfigManager +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.util.MessageUtil -import com.v2ray.ang.util.MmkvManager -import com.v2ray.ang.util.MmkvManager.KEY_ANG_CONFIGS -import com.v2ray.ang.util.MmkvManager.subStorage import com.v2ray.ang.util.SpeedtestUtil import com.v2ray.ang.util.Utils -import com.v2ray.ang.util.V2rayConfigUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.launch -import java.io.File -import java.io.FileOutputStream import java.util.Collections class MainViewModel(application: Application) : AndroidViewModel(application) { private var serverList = MmkvManager.decodeServerList() - var subscriptionId: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty() + var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty() - //var keywordFilter: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")?:"" + //var keywordFilter: String = MmkvManager.MmkvManager.decodeSettingsString(AppConfig.CACHE_KEYWORD_FILTER, "")?:"" var keywordFilter = "" val serversCache = mutableListOf() val isRunning by lazy { MutableLiveData() } @@ -53,20 +45,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { val updateTestResultAction by lazy { MutableLiveData() } 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().registerReceiver( - mMsgReceiver, - IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY), - Context.RECEIVER_EXPORTED - ) - } else { - getApplication().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, "") } @@ -99,21 +86,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { && server.contains("routing") ) { try { - val config = ServerConfig.create(EConfigType.CUSTOM) + val config = CustomFmt.parse(server) ?: return false config.subscriptionId = subscriptionId - config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java) - config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString() val key = MmkvManager.encodeServerConfig("", config) - MmkvManager.serverRawStorage?.encode(key, server) + MmkvManager.encodeServerRaw(key, server) serverList.add(0, key) - val profile = ProfileItem( - configType = config.configType, - subscriptionId = config.subscriptionId, - remarks = config.remarks, - server = config.getProxyOutbound()?.getServerAddress(), - serverPort = config.getProxyOutbound()?.getServerPort(), - ) - serversCache.add(0, ServersCache(key, profile)) +// val profile = ProfileLiteItem( +// configType = config.configType, +// subscriptionId = config.subscriptionId, +// remarks = config.remarks, +// server = config.getProxyOutbound()?.getServerAddress(), +// serverPort = config.getProxyOutbound()?.getServerPort(), +// ) + serversCache.add(0, ServersCache(key, config)) return true } catch (e: Exception) { e.printStackTrace() @@ -123,27 +108,34 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } fun swapServer(fromPosition: Int, toPosition: Int) { - Collections.swap(serverList, fromPosition, toPosition) + if (subscriptionId.isEmpty()) { + Collections.swap(serverList, fromPosition, toPosition) + } else { + val fromPosition2 = serverList.indexOf(serversCache[fromPosition].guid) + val toPosition2 = serverList.indexOf(serversCache[toPosition].guid) + Collections.swap(serverList, fromPosition2, toPosition2) + } Collections.swap(serversCache, fromPosition, toPosition) - MmkvManager.mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList)) + MmkvManager.encodeServerList(serverList) } @Synchronized fun updateCache() { serversCache.clear() for (guid in serverList) { - var profile = MmkvManager.decodeProfileConfig(guid) - if (profile == null) { - val config = MmkvManager.decodeServerConfig(guid) ?: continue - profile = ProfileItem( - configType = config.configType, - subscriptionId = config.subscriptionId, - remarks = config.remarks, - server = config.getProxyOutbound()?.getServerAddress(), - serverPort = config.getProxyOutbound()?.getServerPort(), - ) - MmkvManager.encodeServerConfig(guid, config) - } + var profile = MmkvManager.decodeServerConfig(guid) ?: continue +// var profile = MmkvManager.decodeProfileConfig(guid) +// if (profile == null) { +// val config = MmkvManager.decodeServerConfig(guid) ?: continue +// profile = ProfileLiteItem( +// configType = config.configType, +// subscriptionId = config.subscriptionId, +// remarks = config.remarks, +// server = config.getProxyOutbound()?.getServerAddress(), +// serverPort = config.getProxyOutbound()?.getServerPort(), +// ) +// MmkvManager.encodeServerConfig(guid, config) +// } if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) { continue @@ -156,21 +148,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } fun updateConfigViaSubAll(): Int { - if (subscriptionId.isNullOrEmpty()) { + if (subscriptionId.isEmpty()) { return AngConfigManager.updateConfigViaSubAll() } else { - val json = subStorage?.decodeString(subscriptionId) - if (!json.isNullOrBlank()) { - return updateConfigViaSub(Pair(subscriptionId, Gson().fromJson(json, SubscriptionItem::class.java))) - } else { - return 0 - } + val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return 0 + return AngConfigManager.updateConfigViaSub(Pair(subscriptionId, subItem)) } } fun exportAllServer(): Int { val serverListCopy = - if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) { + if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) { serverList } else { serversCache.map { it.guid }.toList() @@ -188,16 +176,16 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { tcpingTestScope.coroutineContext[Job]?.cancelChildren() SpeedtestUtil.closeAllTcpSockets() MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList()) - updateListAction.value = -1 // update all + //updateListAction.value = -1 // update all - getApplication().toast(R.string.connection_test_testing) - for (item in serversCache) { + val serversCopy = serversCache.toList() // Create a copy of the list + for (item in serversCopy) { item.profile.let { outbound -> val serverAddress = outbound.server val serverPort = outbound.serverPort if (serverAddress != null && serverPort != null) { tcpingTestScope.launch { - val testResult = SpeedtestUtil.tcping(serverAddress, serverPort) + val testResult = SpeedtestUtil.tcping(serverAddress, serverPort.toInt()) launch(Dispatchers.Main) { MmkvManager.encodeServerTestDelayMillis(item.guid, testResult) updateListAction.value = getPosition(item.guid) @@ -214,18 +202,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { updateListAction.value = -1 // update all val serversCopy = serversCache.toList() // Create a copy of the list - - getApplication().toast(R.string.connection_test_testing) viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread for (item in serversCopy) { - val config = V2rayConfigUtil.getV2rayConfig(getApplication(), item.guid) - if (config.status) { - MessageUtil.sendMsg2TestService( - getApplication(), - AppConfig.MSG_MEASURE_CONFIG, - Pair(item.guid, Utils.removeKeepAlive(config.content) ) - ) - } + MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, item.guid) } } } @@ -237,7 +216,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { fun subscriptionIdChanged(id: String) { if (subscriptionId != id) { subscriptionId = id - MmkvManager.settingsStorage.encode(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId) + MmkvManager.encodeSettings(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId) reloadServerList() } } @@ -269,7 +248,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } fun removeDuplicateServer(): Int { - val serversCacheCopy = mutableListOf>() + val serversCacheCopy = mutableListOf>() for (it in serversCache) { val config = MmkvManager.decodeServerConfig(it.guid) ?: continue serversCacheCopy.add(Pair(it.guid, config)) @@ -277,11 +256,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { val deleteServer = mutableListOf() serversCacheCopy.forEachIndexed { index, it -> - val outbound = it.second.getProxyOutbound() + val outbound = it.second serversCacheCopy.forEachIndexed { index2, it2 -> if (index2 > index) { - val outbound2 = it2.second.getProxyOutbound() - if (outbound == outbound2 && !deleteServer.contains(it2.first)) { + val outbound2 = it2.second + if (outbound.equals(outbound2) && !deleteServer.contains(it2.first)) { deleteServer.add(it2.first) } } @@ -294,56 +273,55 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { return deleteServer.count() } - fun removeAllServer() { - if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) { - MmkvManager.removeAllServer() - } else { - val serversCopy = serversCache.toList() - for (item in serversCopy) { - MmkvManager.removeServer(item.guid) + fun removeAllServer(): Int { + val count = + if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) { + MmkvManager.removeAllServer() + } else { + val serversCopy = serversCache.toList() + for (item in serversCopy) { + MmkvManager.removeServer(item.guid) + } + serversCache.toList().count() } - } + return count } - fun removeInvalidServer() { - if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) { - MmkvManager.removeInvalidServer("") + fun removeInvalidServer(): Int { + var count = 0 + if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) { + count += MmkvManager.removeInvalidServer("") } else { val serversCopy = serversCache.toList() for (item in serversCopy) { - MmkvManager.removeInvalidServer(item.guid) + count += MmkvManager.removeInvalidServer(item.guid) } } + return count } fun sortByTestResults() { - MmkvManager.sortByTestResults() - } + data class ServerDelay(var guid: String, var testDelayMillis: Long) + val serverDelays = mutableListOf() + val serverList = MmkvManager.decodeServerList() + serverList.forEach { key -> + val delay = MmkvManager.decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L + serverDelays.add(ServerDelay(key, if (delay <= 0L) 999999 else delay)) + } + serverDelays.sortBy { it.testDelayMillis } + + serverDelays.forEach { + serverList.remove(it.guid) + serverList.add(it.guid) + } - fun copyAssets(assets: AssetManager) { - val extFolder = Utils.userAssetPath(getApplication()) + MmkvManager.encodeServerList(serverList) + } + + fun initAssets(assets: AssetManager) { viewModelScope.launch(Dispatchers.Default) { - try { - val geo = arrayOf("geosite.dat", "geoip.dat") - assets.list("") - ?.filter { geo.contains(it) } - ?.filter { !File(extFolder, it).exists() } - ?.forEach { - val target = File(extFolder, it) - assets.open(it).use { input -> - FileOutputStream(target).use { output -> - input.copyTo(output) - } - } - Log.i( - ANG_PACKAGE, - "Copied from apk assets folder to ${target.absolutePath}" - ) - } - } catch (e: Exception) { - Log.e(ANG_PACKAGE, "asset copy failed", e) - } + SettingsManager.initAssets(getApplication(), assets) } } @@ -352,7 +330,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { return } keywordFilter = keyword - MmkvManager.settingsStorage.encode(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter) + MmkvManager.encodeSettings(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter) reloadServerList() } @@ -386,15 +364,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> { - val resultPair: Pair = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getSerializableExtra("content", Pair::class.java) as Pair - } else { - intent.getSerializableExtra("content") as Pair - } + val resultPair = intent.serializable>("content") ?: return MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second) updateListAction.value = getPosition(resultPair.first) } } } } -} \ No newline at end of file +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/SettingsViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt similarity index 73% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/SettingsViewModel.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt index 9f8d844740..9e73ff39ac 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/SettingsViewModel.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt @@ -5,21 +5,13 @@ import android.content.SharedPreferences import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.preference.PreferenceManager -import com.tencent.mmkv.MMKV import com.v2ray.ang.AppConfig -import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.util.Utils class SettingsViewModel(application: Application) : AndroidViewModel(application), SharedPreferences.OnSharedPreferenceChangeListener { - private val settingsStorage by lazy { - MMKV.mmkvWithID( - MmkvManager.ID_SETTING, - MMKV.MULTI_PROCESS_MODE - ) - } - fun startListenPreferenceChange() { PreferenceManager.getDefaultSharedPreferences(getApplication()) .registerOnSharedPreferenceChangeListener(this) @@ -37,34 +29,33 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application when (key) { AppConfig.PREF_MODE, AppConfig.PREF_VPN_DNS, + AppConfig.PREF_VPN_BYPASS_LAN, AppConfig.PREF_REMOTE_DNS, AppConfig.PREF_DOMESTIC_DNS, + AppConfig.PREF_DNS_HOSTS, AppConfig.PREF_DELAY_TEST_URL, AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PREF_SOCKS_PORT, - AppConfig.PREF_HTTP_PORT, AppConfig.PREF_LOGLEVEL, AppConfig.PREF_LANGUAGE, AppConfig.PREF_UI_MODE_NIGHT, AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, - AppConfig.PREF_ROUTING_MODE, - AppConfig.PREF_V2RAY_ROUTING_AGENT, - AppConfig.PREF_V2RAY_ROUTING_BLOCKED, - AppConfig.PREF_V2RAY_ROUTING_DIRECT, AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.PREF_FRAGMENT_PACKETS, AppConfig.PREF_FRAGMENT_LENGTH, AppConfig.PREF_FRAGMENT_INTERVAL, AppConfig.PREF_MUX_XUDP_QUIC, - -> { - settingsStorage?.encode(key, sharedPreferences.getString(key, "")) + -> { + MmkvManager.encodeSettings(key, sharedPreferences.getString(key, "")) } AppConfig.PREF_ROUTE_ONLY_ENABLED, + AppConfig.PREF_IS_BOOTED, AppConfig.PREF_SPEED_ENABLED, AppConfig.PREF_PROXY_SHARING, AppConfig.PREF_LOCAL_DNS_ENABLED, AppConfig.PREF_FAKE_DNS_ENABLED, + AppConfig.PREF_APPEND_HTTP_PROXY, AppConfig.PREF_ALLOW_INSECURE, AppConfig.PREF_PREFER_IPV6, AppConfig.PREF_PER_APP_PROXY, @@ -74,25 +65,25 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application AppConfig.SUBSCRIPTION_AUTO_UPDATE, AppConfig.PREF_FRAGMENT_ENABLED, AppConfig.PREF_MUX_ENABLED, - -> { - settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false)) + -> { + MmkvManager.encodeSettings(key, sharedPreferences.getBoolean(key, false)) } AppConfig.PREF_SNIFFING_ENABLED -> { - settingsStorage?.encode(key, sharedPreferences.getBoolean(key, true)) + MmkvManager.encodeSettings(key, sharedPreferences.getBoolean(key, true)) } AppConfig.PREF_MUX_CONCURRENCY, AppConfig.PREF_MUX_XUDP_CONCURRENCY -> { - settingsStorage?.encode(key, sharedPreferences.getString(key, "8")) + MmkvManager.encodeSettings(key, sharedPreferences.getString(key, "8")) } // AppConfig.PREF_PER_APP_PROXY_SET -> { -// settingsStorage?.encode(key, sharedPreferences.getStringSet(key, setOf())) +// MmkvManager.encodeSettings(key, sharedPreferences.getStringSet(key, setOf())) // } } if (key == AppConfig.PREF_UI_MODE_NIGHT) { - Utils.setNightMode(getApplication()) + Utils.setNightMode() } } } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ERoutingMode.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ERoutingMode.kt deleted file mode 100644 index 8d882ed662..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ERoutingMode.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.v2ray.ang.dto - -enum class ERoutingMode(val value: String) { - GLOBAL_PROXY("0"), - BYPASS_LAN("1"), - BYPASS_MAINLAND("2"), - BYPASS_LAN_MAINLAND("3"), - GLOBAL_DIRECT("4"); -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt deleted file mode 100644 index f4c3a6f420..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.v2ray.ang.extension - -import android.content.Context -import android.os.Build -import android.widget.Toast -import com.v2ray.ang.AngApplication -import me.drakeet.support.toast.ToastCompat -import org.json.JSONObject -import java.net.URI -import java.net.URLConnection - -val Context.v2RayApplication: AngApplication? - get() = applicationContext as? AngApplication - -fun Context.toast(message: Int) { - ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show() -} - -fun Context.toast(message: CharSequence) { - ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show() -} - -fun JSONObject.putOpt(pair: Pair) { - put(pair.first, pair.second) -} - -fun JSONObject.putOpt(pairs: Map) { - pairs.forEach { put(it.key, it.value) } -} - -const val THRESHOLD = 1000L -const val DIVISOR = 1024.0 - -fun Long.toSpeedString(): String = this.toTrafficString() + "/s" - -fun Long.toTrafficString(): String { - val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB") - var size = this.toDouble() - var unitIndex = 0 - while (size >= THRESHOLD && unitIndex < units.size - 1) { - size /= DIVISOR - unitIndex++ - } - return String.format("%.1f %s", size, units[unitIndex]) -} - -val URLConnection.responseLength: Long - get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - contentLengthLong - } else { - contentLength.toLong() - } - -val URI.idnHost: String - get() = host?.replace("[", "")?.replace("]", "").orEmpty() - -fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "") \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt deleted file mode 100644 index c71294eb04..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.v2ray.ang.ui - -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.text.method.ScrollingMovementMethod -import android.view.Menu -import android.view.MenuItem -import android.view.View -import androidx.lifecycle.lifecycleScope -import com.v2ray.ang.AppConfig.ANG_PACKAGE -import com.v2ray.ang.R -import com.v2ray.ang.databinding.ActivityLogcatBinding -import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.Utils -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.IOException - -class LogcatActivity : BaseActivity() { - private val binding by lazy { - ActivityLogcatBinding.inflate(layoutInflater) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - - title = getString(R.string.title_logcat) - - logcat(false) - } - - private fun logcat(shouldFlushLog: Boolean) { - - try { - binding.pbWaiting.visibility = View.VISIBLE - - lifecycleScope.launch(Dispatchers.Default) { - if (shouldFlushLog) { - val lst = LinkedHashSet() - lst.add("logcat") - lst.add("-c") - withContext(Dispatchers.IO) { - val process = Runtime.getRuntime().exec(lst.toTypedArray()) - process.waitFor() - } - } - val lst = LinkedHashSet() - lst.add("logcat") - lst.add("-d") - lst.add("-v") - lst.add("time") - lst.add("-s") - lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err") - val process = withContext(Dispatchers.IO) { - Runtime.getRuntime().exec(lst.toTypedArray()) - } -// val bufferedReader = BufferedReader( -// InputStreamReader(process.inputStream)) -// val allText = bufferedReader.use(BufferedReader::readText) - val allText = process.inputStream.bufferedReader().use { it.readText() } - launch(Dispatchers.Main) { - binding.tvLogcat.text = allText - binding.tvLogcat.movementMethod = ScrollingMovementMethod() - binding.pbWaiting.visibility = View.GONE - Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) } - } - } - } catch (e: IOException) { - e.printStackTrace() - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_logcat, menu) - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.copy_all -> { - Utils.setClipboard(this, binding.tvLogcat.text.toString()) - toast(R.string.toast_success) - true - } - - R.id.clear_all -> { - logcat(true) - true - } - - else -> super.onOptionsItemSelected(item) - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt deleted file mode 100644 index c8d84ab0cc..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.v2ray.ang.ui - -import android.os.Bundle -import androidx.fragment.app.Fragment -import com.google.android.material.tabs.TabLayoutMediator -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.databinding.ActivityRoutingSettingsBinding - -class RoutingSettingsActivity : BaseActivity() { - private val binding by lazy { ActivityRoutingSettingsBinding.inflate(layoutInflater) } - - private val titles: Array by lazy { - resources.getStringArray(R.array.routing_tag) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - - title = getString(R.string.title_pref_routing_custom) - - val fragments = ArrayList() - fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_AGENT)) - fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_DIRECT)) - fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)) - - val adapter = FragmentAdapter(this, fragments) - binding.viewpager.adapter = adapter - //tablayout.setTabTextColors(Color.BLACK, Color.RED) - TabLayoutMediator(binding.tablayout, binding.viewpager) { tab, position -> - tab.text = titles[position] - }.attach() - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsFragment.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsFragment.kt deleted file mode 100644 index 4904a69c5e..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsFragment.kt +++ /dev/null @@ -1,172 +0,0 @@ -package com.v2ray.ang.ui - -import android.Manifest -import android.content.Intent -import android.os.Bundle -import android.text.TextUtils -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity.RESULT_OK -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import com.tbruyelle.rxpermissions3.RxPermissions -import com.tencent.mmkv.MMKV -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.databinding.FragmentRoutingSettingsBinding -import com.v2ray.ang.extension.toast -import com.v2ray.ang.extension.v2RayApplication -import com.v2ray.ang.util.MmkvManager -import com.v2ray.ang.util.Utils -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -class RoutingSettingsFragment : Fragment() { - private val binding by lazy { FragmentRoutingSettingsBinding.inflate(layoutInflater) } - - companion object { - private const val routing_arg = "routing_arg" - } - - private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - // Inflate the layout for this fragment - return binding.root// inflater.inflate(R.layout.fragment_routing_settings, container, false) - } - - fun newInstance(arg: String): Fragment { - val fragment = RoutingSettingsFragment() - val bundle = Bundle() - bundle.putString(routing_arg, arg) - fragment.arguments = bundle - return fragment - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val content = settingsStorage?.getString(requireArguments().getString(routing_arg), "") - binding.etRoutingContent.text = Utils.getEditable(content) - - setHasOptionsMenu(true) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.menu_routing, menu) - return super.onCreateOptionsMenu(menu, inflater) - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.save_routing -> { - saveRouting() - true - } - - R.id.del_routing -> { - binding.etRoutingContent.text = null - true - } - - R.id.scan_replace -> { - scanQRcode(true) - true - } - - R.id.scan_append -> { - scanQRcode(false) - true - } - - R.id.default_rules -> { - setDefaultRules() - true - } - - else -> super.onOptionsItemSelected(item) - } - - private fun saveRouting() { - val content = binding.etRoutingContent.text.toString() - settingsStorage?.encode(requireArguments().getString(routing_arg), content) - activity?.toast(R.string.toast_success) - } - - fun scanQRcode(forReplace: Boolean): Boolean { -// try { -// startActivityForResult(Intent("com.google.zxing.client.android.SCAN") -// .addCategory(Intent.CATEGORY_DEFAULT) -// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode) -// } catch (e: Exception) { - RxPermissions(requireActivity()) - .request(Manifest.permission.CAMERA) - .subscribe { - if (it) - if (forReplace) - scanQRCodeForReplace.launch(Intent(activity, ScannerActivity::class.java)) - else - scanQRCodeForAppend.launch(Intent(activity, ScannerActivity::class.java)) - else - activity?.toast(R.string.toast_permission_denied) - } -// } - return true - } - - private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - val content = it.data?.getStringExtra("SCAN_RESULT") - binding.etRoutingContent.text = Utils.getEditable(content) - } - } - - private val scanQRCodeForAppend = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - val content = it.data?.getStringExtra("SCAN_RESULT") - binding.etRoutingContent.text = Utils.getEditable("${binding.etRoutingContent.text},$content") - } - } - - fun setDefaultRules(): Boolean { - var url = AppConfig.v2rayCustomRoutingListUrl - var tag = "" - when (requireArguments().getString(routing_arg)) { - AppConfig.PREF_V2RAY_ROUTING_AGENT -> { - tag = AppConfig.TAG_PROXY - } - - AppConfig.PREF_V2RAY_ROUTING_DIRECT -> { - tag = AppConfig.TAG_DIRECT - } - - AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> { - tag = AppConfig.TAG_BLOCKED - } - } - url += tag - - activity?.toast(R.string.msg_downloading_content) - lifecycleScope.launch(Dispatchers.IO) { - val content = Utils.getUrlContext(url, 5000) - launch(Dispatchers.Main) { - val routingList = if (TextUtils.isEmpty(content)) { - Utils.readTextFromAssets(activity?.v2RayApplication, "custom_routing_$tag") - } else { - content - } - binding.etRoutingContent.text = Utils.getEditable(routingList) - saveRouting() - //toast(R.string.toast_success) - } - } - return true - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt deleted file mode 100644 index 43ae8de10b..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt +++ /dev/null @@ -1,695 +0,0 @@ -package com.v2ray.ang.ui - -import android.os.Bundle -import android.text.TextUtils -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.EditText -import android.widget.LinearLayout -import android.widget.Spinner -import android.widget.TextView -import androidx.appcompat.app.AlertDialog -import com.tencent.mmkv.MMKV -import com.v2ray.ang.AppConfig -import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE -import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 -import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6 -import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU -import com.v2ray.ang.AppConfig.WIREGUARD_keep_alive -import com.v2ray.ang.AppConfig.WIREGUARD_wnoise -import com.v2ray.ang.AppConfig.WIREGUARD_wnoisecount -import com.v2ray.ang.AppConfig.WIREGUARD_wnoisedelay -import com.v2ray.ang.AppConfig.WIREGUARD_wpayloadsize - -import com.v2ray.ang.R -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ServerConfig -import com.v2ray.ang.dto.V2rayConfig -import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_PORT -import com.v2ray.ang.dto.V2rayConfig.Companion.TLS -import com.v2ray.ang.extension.removeWhiteSpace -import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.MmkvManager -import com.v2ray.ang.util.MmkvManager.ID_MAIN -import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER -import com.v2ray.ang.util.Utils -import com.v2ray.ang.util.Utils.getIpv6Address - -class ServerActivity : BaseActivity() { - - private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) } - private val settingsStorage by lazy { - MMKV.mmkvWithID( - MmkvManager.ID_SETTING, - MMKV.MULTI_PROCESS_MODE - ) - } - private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() } - private val isRunning by lazy { - intent.getBooleanExtra("isRunning", false) - && editGuid.isNotEmpty() - && editGuid == mainStorage?.decodeString(KEY_SELECTED_SERVER) - } - private val createConfigType by lazy { - EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value)) - ?: EConfigType.VMESS - } - private val subscriptionId by lazy { - intent.getStringExtra("subscriptionId") - } - - private val securitys: Array by lazy { - resources.getStringArray(R.array.securitys) - } - private val shadowsocksSecuritys: Array by lazy { - resources.getStringArray(R.array.ss_securitys) - } - private val flows: Array by lazy { - resources.getStringArray(R.array.flows) - } - private val networks: Array by lazy { - resources.getStringArray(R.array.networks) - } - private val tcpTypes: Array by lazy { - resources.getStringArray(R.array.header_type_tcp) - } - private val kcpAndQuicTypes: Array by lazy { - resources.getStringArray(R.array.header_type_kcp_and_quic) - } - private val grpcModes: Array by lazy { - resources.getStringArray(R.array.mode_type_grpc) - } - private val streamSecuritys: Array by lazy { - resources.getStringArray(R.array.streamsecurityxs) - } - private val allowinsecures: Array by lazy { - resources.getStringArray(R.array.allowinsecures) - } - private val uTlsItems: Array by lazy { - resources.getStringArray(R.array.streamsecurity_utls) - } - private val alpns: Array by lazy { - resources.getStringArray(R.array.streamsecurity_alpn) - } - - // 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 - // protocols. Use findViewById manually ensures the xml are de-coupled with the activity logic. - private val et_remarks: EditText by lazy { findViewById(R.id.et_remarks) } - private val et_address: EditText by lazy { findViewById(R.id.et_address) } - private val et_port: EditText by lazy { findViewById(R.id.et_port) } - private val et_id: EditText by lazy { findViewById(R.id.et_id) } - private val et_alterId: EditText? by lazy { findViewById(R.id.et_alterId) } - private val et_security: EditText? by lazy { findViewById(R.id.et_security) } - private val sp_flow: Spinner? by lazy { findViewById(R.id.sp_flow) } - private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) } - private val sp_stream_security: Spinner? by lazy { findViewById(R.id.sp_stream_security) } - private val sp_allow_insecure: Spinner? by lazy { findViewById(R.id.sp_allow_insecure) } - private val container_allow_insecure: LinearLayout? by lazy { findViewById(R.id.l5) } - private val et_sni: EditText? by lazy { findViewById(R.id.et_sni) } - private val container_sni: LinearLayout? by lazy { findViewById(R.id.l2) } - private val sp_stream_fingerprint: Spinner? by lazy { findViewById(R.id.sp_stream_fingerprint) } //uTLS - private val container_fingerprint: LinearLayout? by lazy { findViewById(R.id.l3) } - private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) } - private val sp_header_type: Spinner? by lazy { findViewById(R.id.sp_header_type) } - private val sp_header_type_title: TextView? by lazy { findViewById(R.id.sp_header_type_title) } - private val tv_request_host: TextView? by lazy { findViewById(R.id.tv_request_host) } - private val et_request_host: EditText? by lazy { findViewById(R.id.et_request_host) } - private val tv_path: TextView? by lazy { findViewById(R.id.tv_path) } - private val et_path: EditText? by lazy { findViewById(R.id.et_path) } - 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.l4) } - private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) } - private val container_public_key: LinearLayout? by lazy { findViewById(R.id.l6) } - 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.l7) } - private val et_spider_x: EditText? by lazy { findViewById(R.id.et_spider_x) } - private val container_spider_x: LinearLayout? by lazy { findViewById(R.id.l8) } - private val et_reserved1: EditText? by lazy { findViewById(R.id.et_reserved1) } - private val et_reserved2: EditText? by lazy { findViewById(R.id.et_reserved2) } - private val et_reserved3: EditText? by lazy { findViewById(R.id.et_reserved3) } - private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) } - private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) } - - private val et_keepalive: EditText? by lazy { findViewById(R.id.et_keepalive) } - private val et_wnoise: EditText? by lazy { findViewById(R.id.et_wnoise) } - private val et_wnoisecount: EditText? by lazy { findViewById(R.id.et_wnoisecount) } - private val et_wnoisedelay: EditText? by lazy { findViewById(R.id.et_wnoisedelay) } - private val et_wpayloadsize: EditText? by lazy { findViewById(R.id.et_wpayloadsize) } - - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - title = getString(R.string.title_server) - - val config = MmkvManager.decodeServerConfig(editGuid) - when (config?.configType ?: createConfigType) { - EConfigType.VMESS -> setContentView(R.layout.activity_server_vmess) - EConfigType.CUSTOM -> return - EConfigType.SHADOWSOCKS -> setContentView(R.layout.activity_server_shadowsocks) - EConfigType.SOCKS -> setContentView(R.layout.activity_server_socks) - EConfigType.VLESS -> setContentView(R.layout.activity_server_vless) - EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan) - EConfigType.WIREGUARD -> setContentView(R.layout.activity_server_wireguard) - } - sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - val types = transportTypes(networks[position]) - sp_header_type?.isEnabled = types.size > 1 - val adapter = - 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?.getProxyOutbound()?.getTransportSettingDetails()?.let { transportDetails -> - sp_header_type?.setSelection(Utils.arrayFind(types, transportDetails[0])) - et_request_host?.text = Utils.getEditable(transportDetails[1]) - et_path?.text = Utils.getEditable(transportDetails[2]) - } - - 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 - else -> R.string.server_lab_request_host - } - ) - ) - - 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 - else -> R.string.server_lab_path - } - ) - ) - } - - override fun onNothingSelected(parent: AdapterView<*>?) { - // do nothing - } - } - sp_stream_security?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - if (streamSecuritys[position].isBlank()) { - container_sni?.visibility = View.GONE - container_fingerprint?.visibility = View.GONE - container_alpn?.visibility = View.GONE - container_allow_insecure?.visibility = View.GONE - container_public_key?.visibility = View.GONE - container_short_id?.visibility = View.GONE - container_spider_x?.visibility = View.GONE - } else { - container_sni?.visibility = View.VISIBLE - container_fingerprint?.visibility = View.VISIBLE - container_alpn?.visibility = View.VISIBLE - if (streamSecuritys[position] == TLS) { - container_allow_insecure?.visibility = View.VISIBLE - container_public_key?.visibility = View.GONE - container_short_id?.visibility = View.GONE - container_spider_x?.visibility = View.GONE - } else { - container_allow_insecure?.visibility = View.GONE - container_alpn?.visibility = View.GONE - container_public_key?.visibility = View.VISIBLE - container_short_id?.visibility = View.VISIBLE - container_spider_x?.visibility = View.VISIBLE - } - } - } - - override fun onNothingSelected(p0: AdapterView<*>?) { - // do nothing - } - } - if (config != null) { - bindingServer(config) - } else { - clearServer() - } - } - - /** - * binding selected server config - */ - private fun bindingServer(config: ServerConfig): Boolean { - val outbound = config.getProxyOutbound() ?: return false - - et_remarks.text = Utils.getEditable(config.remarks) - et_address.text = Utils.getEditable(outbound.getServerAddress().orEmpty()) - et_port.text = - Utils.getEditable(outbound.getServerPort()?.toString() ?: DEFAULT_PORT.toString()) - et_id.text = Utils.getEditable(outbound.getPassword().orEmpty()) - et_alterId?.text = - Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()) - if (config.configType == EConfigType.SOCKS) { - et_security?.text = - Utils.getEditable(outbound.settings?.servers?.get(0)?.users?.get(0)?.user.orEmpty()) - } else if (config.configType == EConfigType.VLESS) { - et_security?.text = Utils.getEditable(outbound.getSecurityEncryption().orEmpty()) - val flow = Utils.arrayFind( - flows, - outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow.orEmpty() - ) - if (flow >= 0) { - sp_flow?.setSelection(flow) - } - } else if (config.configType == EConfigType.WIREGUARD) { - et_public_key?.text = - Utils.getEditable(outbound.settings?.peers?.get(0)?.publicKey.orEmpty()) - if (outbound.settings?.reserved == null) { - et_reserved1?.text = Utils.getEditable("0") - et_reserved2?.text = Utils.getEditable("0") - et_reserved3?.text = Utils.getEditable("0") - } else { - et_reserved1?.text = - Utils.getEditable(outbound.settings?.reserved?.get(0).toString()) - et_reserved2?.text = - Utils.getEditable(outbound.settings?.reserved?.get(1).toString()) - et_reserved3?.text = - Utils.getEditable(outbound.settings?.reserved?.get(2).toString()) - } - if (outbound.settings?.address == null) { - et_local_address?.text = - Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}") - } else { - val list = outbound.settings?.address as List<*> - et_local_address?.text = Utils.getEditable(list.joinToString()) - } - if (outbound.settings?.mtu == null) { - et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU) - } else { - et_local_mtu?.text = Utils.getEditable(outbound.settings?.mtu.toString()) - } - - - if (outbound.settings?.peers?.get(0)?.keepAlive == null) { - et_keepalive?.text = Utils.getEditable(WIREGUARD_keep_alive) - } else { - et_keepalive?.text = Utils.getEditable(outbound.settings?.peers?.get(0)?.keepAlive.toString()) - } - - if (outbound.settings?.wnoise == null) { - et_wnoise?.text = Utils.getEditable(WIREGUARD_wnoise) - } else { - et_wnoise?.text = Utils.getEditable(outbound.settings?.wnoise.orEmpty()) - } - - if (outbound.settings?.wnoisecount == null) { - et_wnoisecount?.text = Utils.getEditable(WIREGUARD_wnoisecount) - } else { - et_wnoisecount?.text = Utils.getEditable(outbound.settings?.wnoisecount.orEmpty()) - } - - if (outbound.settings?.wnoisedelay == null) { - et_wnoisedelay?.text = Utils.getEditable(WIREGUARD_wnoisedelay) - } else { - et_wnoisedelay?.text = Utils.getEditable(outbound.settings?.wnoisedelay.orEmpty()) - } - - if (outbound.settings?.wpayloadsize == null) { - et_wpayloadsize?.text = Utils.getEditable(WIREGUARD_wpayloadsize) - } else { - et_wpayloadsize?.text = Utils.getEditable(outbound.settings?.wpayloadsize.orEmpty()) - } - - } - val securityEncryptions = - if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys - val security = - Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty()) - if (security >= 0) { - sp_security?.setSelection(security) - } - - val streamSetting = config.outboundBean?.streamSettings ?: return true - val streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security) - if (streamSecurity >= 0) { - sp_stream_security?.setSelection(streamSecurity) - (streamSetting.tlsSettings ?: streamSetting.realitySettings)?.let { tlsSetting -> - container_sni?.visibility = View.VISIBLE - container_fingerprint?.visibility = View.VISIBLE - container_alpn?.visibility = View.VISIBLE - et_sni?.text = Utils.getEditable(tlsSetting.serverName) - tlsSetting.fingerprint?.let { - val utlsIndex = Utils.arrayFind(uTlsItems, tlsSetting.fingerprint) - sp_stream_fingerprint?.setSelection(utlsIndex) - } - tlsSetting.alpn?.let { - val alpnIndex = Utils.arrayFind( - alpns, - Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty() - ) - sp_stream_alpn?.setSelection(alpnIndex) - } - if (streamSetting.tlsSettings != null) { - container_allow_insecure?.visibility = View.VISIBLE - val allowinsecure = - Utils.arrayFind(allowinsecures, tlsSetting.allowInsecure.toString()) - if (allowinsecure >= 0) { - sp_allow_insecure?.setSelection(allowinsecure) - } - container_public_key?.visibility = View.GONE - container_short_id?.visibility = View.GONE - container_spider_x?.visibility = View.GONE - } else { // reality settings - container_public_key?.visibility = View.VISIBLE - et_public_key?.text = Utils.getEditable(tlsSetting.publicKey.orEmpty()) - container_short_id?.visibility = View.VISIBLE - et_short_id?.text = Utils.getEditable(tlsSetting.shortId.orEmpty()) - container_spider_x?.visibility = View.VISIBLE - et_spider_x?.text = Utils.getEditable(tlsSetting.spiderX.orEmpty()) - container_allow_insecure?.visibility = View.GONE - } - } - if (streamSetting.tlsSettings == null && streamSetting.realitySettings == null) { - container_sni?.visibility = View.GONE - container_fingerprint?.visibility = View.GONE - container_alpn?.visibility = View.GONE - container_allow_insecure?.visibility = View.GONE - container_public_key?.visibility = View.GONE - container_short_id?.visibility = View.GONE - container_spider_x?.visibility = View.GONE - } - } - val network = Utils.arrayFind(networks, streamSetting.network) - if (network >= 0) { - sp_network?.setSelection(network) - } - return true - } - - /** - * clear or init server config - */ - private fun clearServer(): Boolean { - et_remarks.text = null - et_address.text = null - et_port.text = Utils.getEditable(DEFAULT_PORT.toString()) - et_id.text = null - et_alterId?.text = Utils.getEditable("0") - sp_security?.setSelection(0) - sp_network?.setSelection(0) - - sp_header_type?.setSelection(0) - et_request_host?.text = null - et_path?.text = null - sp_stream_security?.setSelection(0) - sp_allow_insecure?.setSelection(0) - et_sni?.text = null - - //et_security.text = null - sp_flow?.setSelection(0) - et_public_key?.text = null - et_reserved1?.text = Utils.getEditable("0") - et_reserved2?.text = Utils.getEditable("0") - et_reserved3?.text = Utils.getEditable("0") - et_local_address?.text = - Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}") - et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU) - return true - } - - /** - * save server config - */ - private fun saveServer(): Boolean { - if (TextUtils.isEmpty(et_remarks.text.toString())) { - toast(R.string.server_lab_remarks) - return false - } - if (TextUtils.isEmpty(et_address.text.toString())) { - toast(R.string.server_lab_address) - return false - } - val port = Utils.parseInt(et_port.text.toString()) - if (port <= 0) { - toast(R.string.server_lab_port) - return false - } - val config = - MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(createConfigType) - if (config.configType != EConfigType.SOCKS && TextUtils.isEmpty(et_id.text.toString())) { - if (config.configType == EConfigType.TROJAN || config.configType == EConfigType.SHADOWSOCKS) { - toast(R.string.server_lab_id3) - } else { - toast(R.string.server_lab_id) - } - return false - } - sp_stream_security?.let { - if (config.configType == EConfigType.TROJAN && TextUtils.isEmpty(streamSecuritys[it.selectedItemPosition])) { - toast(R.string.server_lab_stream_security) - return false - } - } - et_alterId?.let { - val alterId = Utils.parseInt(it.text.toString()) - if (alterId < 0) { - toast(R.string.server_lab_alterid) - return false - } - } - - config.remarks = et_remarks.text.toString().trim() - config.outboundBean?.settings?.vnext?.get(0)?.let { vnext -> - saveVnext(vnext, port, config) - } - config.outboundBean?.settings?.servers?.get(0)?.let { server -> - saveServers(server, port, config) - } - val wireguard = config.outboundBean?.settings - wireguard?.peers?.get(0)?.let { _ -> - savePeer(wireguard, port) - } - config.outboundBean?.streamSettings?.let { - saveStreamSettings(it) - } - if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) { - config.subscriptionId = subscriptionId.orEmpty() - } - - MmkvManager.encodeServerConfig(editGuid, config) - toast(R.string.toast_success) - finish() - return true - } - - private fun saveVnext( - vnext: V2rayConfig.OutboundBean.OutSettingsBean.VnextBean, - port: Int, - config: ServerConfig - ) { - vnext.address = et_address.text.toString().trim() - vnext.port = port - vnext.users[0].id = et_id.text.toString().trim() - if (config.configType == EConfigType.VMESS) { - vnext.users[0].alterId = Utils.parseInt(et_alterId?.text.toString()) - vnext.users[0].security = securitys[sp_security?.selectedItemPosition ?: 0] - } else if (config.configType == EConfigType.VLESS) { - vnext.users[0].encryption = et_security?.text.toString().trim() - vnext.users[0].flow = flows[sp_flow?.selectedItemPosition ?: 0] - } - } - - private fun saveServers( - server: V2rayConfig.OutboundBean.OutSettingsBean.ServersBean, - port: Int, - config: ServerConfig - ) { - server.address = et_address.text.toString().trim() - server.port = port - if (config.configType == EConfigType.SHADOWSOCKS) { - server.password = et_id.text.toString().trim() - server.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0] - } else if (config.configType == EConfigType.SOCKS) { - if (TextUtils.isEmpty(et_security?.text) && TextUtils.isEmpty(et_id.text)) { - server.users = null - } else { - val socksUsersBean = - V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() - socksUsersBean.user = et_security?.text.toString().trim() - socksUsersBean.pass = et_id.text.toString().trim() - server.users = listOf(socksUsersBean) - } - } else if (config.configType == EConfigType.TROJAN) { - server.password = et_id.text.toString().trim() - } - } - - private fun savePeer(wireguard: V2rayConfig.OutboundBean.OutSettingsBean, port: Int) { - wireguard.secretKey = et_id.text.toString().trim() - wireguard.peers?.get(0)?.publicKey = et_public_key?.text.toString().trim() - wireguard.peers?.get(0)?.endpoint = - getIpv6Address(et_address.text.toString().trim()) + ":" + port - val reserved1 = Utils.parseInt(et_reserved1?.text.toString()) - val reserved2 = Utils.parseInt(et_reserved2?.text.toString()) - val reserved3 = Utils.parseInt(et_reserved3?.text.toString()) - if (reserved1 > 0 || reserved2 > 0 || reserved3 > 0) { - wireguard.reserved = listOf(reserved1, reserved2, reserved3) - } else { - wireguard.reserved = null - } - wireguard.address = et_local_address?.text.toString().removeWhiteSpace().split(",") - wireguard.mtu = Utils.parseInt(et_local_mtu?.text.toString()) - - wireguard.peers?.get(0)?.keepAlive = Utils.parseInt(et_keepalive?.text.toString()) - wireguard.wnoise = et_wnoise?.text.toString().trim() - wireguard.wnoisecount = et_wnoisecount?.text.toString().trim() - wireguard.wnoisedelay = et_wnoisedelay?.text.toString().trim() - wireguard.wpayloadsize = et_wpayloadsize?.text.toString().trim() - } - - private fun saveStreamSettings(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean) { - val network = sp_network?.selectedItemPosition ?: return - val type = sp_header_type?.selectedItemPosition ?: return - val requestHost = et_request_host?.text?.toString()?.trim() ?: return - val path = et_path?.text?.toString()?.trim() ?: return - val sniField = et_sni?.text?.toString()?.trim() ?: return - val allowInsecureField = sp_allow_insecure?.selectedItemPosition ?: return - val streamSecurity = sp_stream_security?.selectedItemPosition ?: return - val utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: return - val alpnIndex = sp_stream_alpn?.selectedItemPosition ?: return - val publicKey = et_public_key?.text?.toString()?.trim() ?: return - val shortId = et_short_id?.text?.toString()?.trim() ?: return - val spiderX = et_spider_x?.text?.toString()?.trim() ?: return - - var sni = streamSetting.populateTransportSettings( - transport = networks[network], - headerType = transportTypes(networks[network])[type], - host = requestHost, - path = path, - seed = path, - quicSecurity = requestHost, - key = path, - mode = transportTypes(networks[network])[type], - serviceName = path, - authority = requestHost, - ) - if (sniField.isNotBlank()) { - sni = sniField - } - val allowInsecure = if (allowinsecures[allowInsecureField].isBlank()) { - !(settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false) - } else { - allowinsecures[allowInsecureField].toBoolean() - } - - streamSetting.populateTlsSettings( - streamSecurity = streamSecuritys[streamSecurity], - allowInsecure = allowInsecure, - sni = sni, - fingerprint = uTlsItems[utlsIndex], - alpns = alpns[alpnIndex], - publicKey = publicKey, - shortId = shortId, - spiderX = spiderX - ) - } - - private fun transportTypes(network: String?): Array { - return when (network) { - "tcp" -> { - tcpTypes - } - - "kcp", "quic" -> { - kcpAndQuicTypes - } - - "grpc" -> { - grpcModes - } - - else -> { - arrayOf("---") - } - } - } - - /** - * save server config - */ - private fun deleteServer(): Boolean { - if (editGuid.isNotEmpty()) { - if (editGuid != mainStorage?.decodeString(KEY_SELECTED_SERVER)) { - if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { - AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) - .setPositiveButton(android.R.string.ok) { _, _ -> - MmkvManager.removeServer(editGuid) - finish() - } - .setNegativeButton(android.R.string.no) { _, _ -> - // do nothing - } - .show() - } else { - MmkvManager.removeServer(editGuid) - finish() - } - } else { - application.toast(R.string.toast_action_not_allowed) - } - } - return true - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.action_server, menu) - val delButton = menu.findItem(R.id.del_config) - val saveButton = menu.findItem(R.id.save_config) - - if (editGuid.isNotEmpty()) { - if (isRunning) { - delButton?.isVisible = false - saveButton?.isVisible = false - } - } else { - delButton?.isVisible = false - } - - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.del_config -> { - deleteServer() - true - } - - R.id.save_config -> { - saveServer() - true - } - - else -> super.onOptionsItemSelected(item) - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt deleted file mode 100644 index beda150fee..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt +++ /dev/null @@ -1,592 +0,0 @@ -package com.v2ray.ang.util - -import android.content.Context -import android.graphics.Bitmap -import android.text.TextUtils -import android.util.Log -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import com.google.gson.JsonPrimitive -import com.google.gson.JsonSerializationContext -import com.google.gson.JsonSerializer -import com.google.gson.reflect.TypeToken -import com.tencent.mmkv.MMKV -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.dto.* -import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER -import com.v2ray.ang.util.fmt.ShadowsocksFmt -import com.v2ray.ang.util.fmt.SocksFmt -import com.v2ray.ang.util.fmt.TrojanFmt -import com.v2ray.ang.util.fmt.VlessFmt -import com.v2ray.ang.util.fmt.VmessFmt -import com.v2ray.ang.util.fmt.WireguardFmt -import java.lang.reflect.Type -import java.util.* - -object AngConfigManager { - private val mainStorage by lazy { - MMKV.mmkvWithID( - MmkvManager.ID_MAIN, - MMKV.MULTI_PROCESS_MODE - ) - } - private val serverRawStorage by lazy { - MMKV.mmkvWithID( - MmkvManager.ID_SERVER_RAW, - MMKV.MULTI_PROCESS_MODE - ) - } - private val settingsStorage by lazy { - MMKV.mmkvWithID( - MmkvManager.ID_SETTING, - MMKV.MULTI_PROCESS_MODE - ) - } - private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) } - - /** - * Legacy loading config - */ -// fun migrateLegacyConfig(c: Context): Boolean? { -// try { -// val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(c) -// val context = defaultSharedPreferences.getString(ANG_CONFIG, "") -// if (context.isNullOrBlank()) { -// return null -// } -// val angConfig = Gson().fromJson(context, AngConfig::class.java) -// for (i in angConfig.vmess.indices) { -// upgradeServerVersion(angConfig.vmess[i]) -// } -// -// copyLegacySettings(defaultSharedPreferences) -// migrateVmessBean(angConfig, defaultSharedPreferences) -// migrateSubItemBean(angConfig) -// -// defaultSharedPreferences.edit().remove(ANG_CONFIG).apply() -// return true -// } catch (e: Exception) { -// e.printStackTrace() -// } -// return false -// } -// -// private fun copyLegacySettings(sharedPreferences: SharedPreferences) { -// listOf( -// AppConfig.PREF_MODE, -// AppConfig.PREF_REMOTE_DNS, -// AppConfig.PREF_DOMESTIC_DNS, -// AppConfig.PREF_LOCAL_DNS_PORT, -// AppConfig.PREF_SOCKS_PORT, -// AppConfig.PREF_HTTP_PORT, -// AppConfig.PREF_LOGLEVEL, -// AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, -// AppConfig.PREF_ROUTING_MODE, -// AppConfig.PREF_V2RAY_ROUTING_AGENT, -// AppConfig.PREF_V2RAY_ROUTING_BLOCKED, -// AppConfig.PREF_V2RAY_ROUTING_DIRECT, -// ).forEach { key -> -// settingsStorage?.encode(key, sharedPreferences.getString(key, null)) -// } -// listOf( -// AppConfig.PREF_SPEED_ENABLED, -// AppConfig.PREF_PROXY_SHARING, -// AppConfig.PREF_LOCAL_DNS_ENABLED, -// AppConfig.PREF_ALLOW_INSECURE, -// AppConfig.PREF_PREFER_IPV6, -// AppConfig.PREF_PER_APP_PROXY, -// AppConfig.PREF_BYPASS_APPS, -// ).forEach { key -> -// settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false)) -// } -// settingsStorage?.encode( -// AppConfig.PREF_SNIFFING_ENABLED, -// sharedPreferences.getBoolean(AppConfig.PREF_SNIFFING_ENABLED, true) -// ) -// settingsStorage?.encode( -// AppConfig.PREF_PER_APP_PROXY_SET, -// sharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, setOf()) -// ) -// } -// -// private fun migrateVmessBean(angConfig: AngConfig, sharedPreferences: SharedPreferences) { -// angConfig.vmess.forEachIndexed { index, vmessBean -> -// val type = EConfigType.fromInt(vmessBean.configType) ?: return@forEachIndexed -// val config = ServerConfig.create(type) -// config.remarks = vmessBean.remarks -// config.subscriptionId = vmessBean.subid -// if (type == EConfigType.CUSTOM) { -// val jsonConfig = sharedPreferences.getString(ANG_CONFIG + vmessBean.guid, "") -// val v2rayConfig = try { -// Gson().fromJson(jsonConfig, V2rayConfig::class.java) -// } catch (e: Exception) { -// e.printStackTrace() -// return@forEachIndexed -// } -// config.fullConfig = v2rayConfig -// serverRawStorage?.encode(vmessBean.guid, jsonConfig) -// } else { -// config.outboundBean?.settings?.vnext?.get(0)?.let { vnext -> -// vnext.address = vmessBean.address -// vnext.port = vmessBean.port -// vnext.users[0].id = vmessBean.id -// if (config.configType == EConfigType.VMESS) { -// vnext.users[0].alterId = vmessBean.alterId -// vnext.users[0].security = vmessBean.security -// } else if (config.configType == EConfigType.VLESS) { -// vnext.users[0].encryption = vmessBean.security -// vnext.users[0].flow = vmessBean.flow -// } -// } -// config.outboundBean?.settings?.servers?.get(0)?.let { server -> -// server.address = vmessBean.address -// server.port = vmessBean.port -// if (config.configType == EConfigType.SHADOWSOCKS) { -// server.password = vmessBean.id -// server.method = vmessBean.security -// } else if (config.configType == EConfigType.SOCKS) { -// if (TextUtils.isEmpty(vmessBean.security) && TextUtils.isEmpty(vmessBean.id)) { -// server.users = null -// } else { -// val socksUsersBean = -// V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() -// socksUsersBean.user = vmessBean.security -// socksUsersBean.pass = vmessBean.id -// server.users = listOf(socksUsersBean) -// } -// } else if (config.configType == EConfigType.TROJAN) { -// server.password = vmessBean.id -// } -// } -// config.outboundBean?.streamSettings?.let { streamSetting -> -// val sni = streamSetting.populateTransportSettings( -// vmessBean.network, -// vmessBean.headerType, -// vmessBean.requestHost, -// vmessBean.path, -// vmessBean.path, -// vmessBean.requestHost, -// vmessBean.path, -// vmessBean.headerType, -// vmessBean.path, -// vmessBean.requestHost, -// ) -// val allowInsecure = if (vmessBean.allowInsecure.isBlank()) { -// settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false -// } else { -// vmessBean.allowInsecure.toBoolean() -// } -// var fingerprint = streamSetting.tlsSettings?.fingerprint -// streamSetting.populateTlsSettings( -// vmessBean.streamSecurity, allowInsecure, -// vmessBean.sni.ifBlank { sni }, fingerprint, null, null, null, null -// ) -// } -// } -// val key = MmkvManager.encodeServerConfig(vmessBean.guid, config) -// if (index == angConfig.index) { -// mainStorage?.encode(KEY_SELECTED_SERVER, key) -// } -// } -// } -// -// private fun migrateSubItemBean(angConfig: AngConfig) { -// angConfig.subItem.forEach { -// val subItem = SubscriptionItem() -// subItem.remarks = it.remarks -// subItem.url = it.url -// subItem.enabled = it.enabled -// subStorage?.encode(it.id, Gson().toJson(subItem)) -// } -// } - - /** - * parse config form qrcode or... - */ - private fun parseConfig( - str: String?, - subid: String, - removedSelectedServer: ServerConfig? - ): Int { - try { - if (str == null || TextUtils.isEmpty(str)) { - return R.string.toast_none_data - } - - val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) { - VmessFmt.parseVmess(str) - } else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) { - ShadowsocksFmt.parseShadowsocks(str) - } else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) { - SocksFmt.parseSocks(str) - } else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) { - TrojanFmt.parseTrojan(str) - } else if (str.startsWith(EConfigType.VLESS.protocolScheme)) { - VlessFmt.parseVless(str) - } else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) { - WireguardFmt.parseWireguard(str) - } else { - null - } - - if (config == null) { - return R.string.toast_incorrect_protocol - } - config.subscriptionId = subid - val guid = MmkvManager.encodeServerConfig("", config) - if (removedSelectedServer != null && - config.getProxyOutbound() - ?.getServerAddress() == removedSelectedServer.getProxyOutbound() - ?.getServerAddress() && - config.getProxyOutbound() - ?.getServerPort() == removedSelectedServer.getProxyOutbound() - ?.getServerPort() - ) { - mainStorage?.encode(KEY_SELECTED_SERVER, guid) - } - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - /** - * share config - */ - private fun shareConfig(guid: String): String { - try { - val config = MmkvManager.decodeServerConfig(guid) ?: return "" - - return config.configType.protocolScheme + when (config.configType) { - EConfigType.VMESS -> VmessFmt.toUri(config) - EConfigType.CUSTOM -> "" - EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config) - EConfigType.SOCKS -> SocksFmt.toUri(config) - EConfigType.VLESS -> VlessFmt.toUri(config) - EConfigType.TROJAN -> TrojanFmt.toUri(config) - EConfigType.WIREGUARD -> WireguardFmt.toUri(config) - } - } catch (e: Exception) { - e.printStackTrace() - return "" - } - } - - /** - * share2Clipboard - */ - fun share2Clipboard(context: Context, guid: String): Int { - try { - val conf = shareConfig(guid) - if (TextUtils.isEmpty(conf)) { - return -1 - } - - Utils.setClipboard(context, conf) - - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - /** - * share2Clipboard - */ - fun shareNonCustomConfigsToClipboard(context: Context, serverList: List): Int { - try { - val sb = StringBuilder() - for (guid in serverList) { - val url = shareConfig(guid) - if (TextUtils.isEmpty(url)) { - continue - } - sb.append(url) - sb.appendLine() - } - if (sb.count() > 0) { - Utils.setClipboard(context, sb.toString()) - } - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - /** - * share2QRCode - */ - fun share2QRCode(guid: String): Bitmap? { - try { - val conf = shareConfig(guid) - if (TextUtils.isEmpty(conf)) { - return null - } - return QRCodeDecoder.createQRCode(conf) - - } catch (e: Exception) { - e.printStackTrace() - return null - } - } - - /** - * shareFullContent2Clipboard - */ - fun shareFullContent2Clipboard(context: Context, guid: String?): Int { - try { - if (guid == null) return -1 - val result = V2rayConfigUtil.getV2rayConfig(context, guid) - if (result.status) { - Utils.setClipboard(context, result.content) - } else { - return -1 - } - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - -// /** -// * upgrade -// */ -// private fun upgradeServerVersion(vmess: AngConfig.VmessBean): Int { -// try { -// if (vmess.configVersion == 2) { -// return 0 -// } -// -// when (vmess.network) { -// "ws", "h2" -> { -// var path = "" -// var host = "" -// val lstParameter = vmess.requestHost.split(";") -// if (lstParameter.isNotEmpty()) { -// path = lstParameter[0].trim() -// } -// if (lstParameter.size > 1) { -// path = lstParameter[0].trim() -// host = lstParameter[1].trim() -// } -// vmess.path = path -// vmess.requestHost = host -// } -// } -// vmess.configVersion = 2 -// return 0 -// } catch (e: Exception) { -// e.printStackTrace() -// return -1 -// } -// } - - fun importBatchConfig(server: String?, subid: String, append: Boolean): Pair { - var count = parseBatchConfig(Utils.decode(server), subid, append) - if (count <= 0) { - count = parseBatchConfig(server, subid, append) - } - if (count <= 0) { - count = parseCustomConfigServer(server, subid) - } - - var countSub = parseBatchSubscription(server) - if (countSub <= 0) { - countSub = parseBatchSubscription(Utils.decode(server)) - } - if (countSub > 0) { - updateConfigViaSubAll() - } - - return count to countSub - } - - fun parseBatchSubscription(servers: String?): Int { - try { - if (servers == null) { - return 0 - } - - var count = 0 - servers.lines() - .forEach { str -> - if (str.startsWith(AppConfig.PROTOCOL_HTTP) || str.startsWith(AppConfig.PROTOCOL_HTTPS)) { - count += MmkvManager.importUrlAsSubscription(str) - } - } - return count - } catch (e: Exception) { - e.printStackTrace() - } - return 0 - } - - fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int { - try { - if (servers == null) { - return 0 - } - val removedSelectedServer = - if (!TextUtils.isEmpty(subid) && !append) { - MmkvManager.decodeServerConfig( - mainStorage?.decodeString(KEY_SELECTED_SERVER).orEmpty() - )?.let { - if (it.subscriptionId == subid) { - return@let it - } - return@let null - } - } else { - null - } - if (!append) { - MmkvManager.removeServerViaSubid(subid) - } - - var count = 0 - servers.lines() - .reversed() - .forEach { - val resId = parseConfig(it, subid, removedSelectedServer) - if (resId == 0) { - count++ - } - } - return count - } catch (e: Exception) { - e.printStackTrace() - } - return 0 - } - - fun parseCustomConfigServer(server: String?, subid: String): Int { - if (server == null) { - return 0 - } - if (server.contains("inbounds") - && server.contains("outbounds") - && server.contains("routing") - ) { - try { - //val gson = GsonBuilder().setPrettyPrinting().create() - val gson = GsonBuilder() - .setPrettyPrinting() - .disableHtmlEscaping() - .registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start - object : TypeToken() {}.type, - JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> - JsonPrimitive( - src?.toInt() - ) - } - ) - .create() - val serverList: Array = - Gson().fromJson(server, Array::class.java) - - if (serverList.isNotEmpty()) { - var count = 0 - for (srv in serverList.reversed()) { - val config = ServerConfig.create(EConfigType.CUSTOM) - config.fullConfig = - Gson().fromJson(Gson().toJson(srv), V2rayConfig::class.java) - config.remarks = config.fullConfig?.remarks - ?: ("%04d-".format(count + 1) + System.currentTimeMillis() - .toString()) - config.subscriptionId = subid - val key = MmkvManager.encodeServerConfig("", config) - serverRawStorage?.encode(key, gson.toJson(srv)) - count += 1 - } - return count - } - } catch (e: Exception) { - e.printStackTrace() - } - - // For compatibility - val config = ServerConfig.create(EConfigType.CUSTOM) - config.subscriptionId = subid - config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java) - config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString() - val key = MmkvManager.encodeServerConfig("", config) - serverRawStorage?.encode(key, server) - return 1 - } else { - return 0 - } - } - - fun updateConfigViaSubAll(): Int { - var count = 0 - try { - MmkvManager.decodeSubscriptions().forEach { - count += updateConfigViaSub(it) - } - } catch (e: Exception) { - e.printStackTrace() - return 0 - } - return count - } - - fun updateConfigViaSub(it: Pair): Int { - try { - if (TextUtils.isEmpty(it.first) - || TextUtils.isEmpty(it.second.remarks) - || TextUtils.isEmpty(it.second.url) - ) { - return 0 - } - if (!it.second.enabled) { - return 0 - } - val url = Utils.idnToASCII(it.second.url) - if (!Utils.isValidUrl(url)) { - return 0 - } - Log.d(AppConfig.ANG_PACKAGE, url) - var configText = try { - Utils.getUrlContentWithCustomUserAgent(url) - } catch (e: Exception) { - e.printStackTrace() - "" - } - if (configText.isEmpty()) { - configText = try { - val httpPort = Utils.parseInt( - settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), - AppConfig.PORT_HTTP.toInt() - ) - Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort) - } catch (e: Exception) { - e.printStackTrace() - "" - } - } - if (configText.isEmpty()) { - return 0 - } - return parseConfigViaSub(configText, it.first, false) - } catch (e: Exception) { - e.printStackTrace() - return 0 - } - } - - private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int { - var count = parseBatchConfig(Utils.decode(server), subid, append) - if (count <= 0) { - count = parseBatchConfig(server, subid, append) - } - if (count <= 0) { - count = parseCustomConfigServer(server, subid) - } - return count - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt deleted file mode 100644 index c04bdfc36a..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt +++ /dev/null @@ -1,232 +0,0 @@ -package com.v2ray.ang.util - -import com.google.gson.Gson -import com.tencent.mmkv.MMKV -import com.v2ray.ang.dto.AssetUrlItem -import com.v2ray.ang.dto.ProfileItem -import com.v2ray.ang.dto.ServerAffiliationInfo -import com.v2ray.ang.dto.ServerConfig -import com.v2ray.ang.dto.SubscriptionItem -import java.net.URI - -object MmkvManager { - const val ID_MAIN = "MAIN" - const val ID_SERVER_CONFIG = "SERVER_CONFIG" - const val ID_PROFILE_CONFIG = "PROFILE_CONFIG" - const val ID_SERVER_RAW = "SERVER_RAW" - const val ID_SERVER_AFF = "SERVER_AFF" - const val ID_SUB = "SUB" - const val ID_ASSET = "ASSET" - const val ID_SETTING = "SETTING" - const val KEY_SELECTED_SERVER = "SELECTED_SERVER" - const val KEY_ANG_CONFIGS = "ANG_CONFIGS" - - val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) } - val settingsStorage by lazy { MMKV.mmkvWithID(ID_SETTING, MMKV.MULTI_PROCESS_MODE) } - private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) } - private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) } - private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) } - val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) } - private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) } - val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) } - - fun decodeServerList(): MutableList { - val json = mainStorage?.decodeString(KEY_ANG_CONFIGS) - return if (json.isNullOrBlank()) { - mutableListOf() - } else { - Gson().fromJson(json, Array::class.java).toMutableList() - } - } - - fun decodeServerConfig(guid: String): ServerConfig? { - if (guid.isBlank()) { - return null - } - val json = serverStorage?.decodeString(guid) - if (json.isNullOrBlank()) { - return null - } - return Gson().fromJson(json, ServerConfig::class.java) - } - - fun decodeProfileConfig(guid: String): ProfileItem? { - if (guid.isBlank()) { - return null - } - val json = profileStorage?.decodeString(guid) - if (json.isNullOrBlank()) { - return null - } - return Gson().fromJson(json, ProfileItem::class.java) - } - - fun encodeServerConfig(guid: String, config: ServerConfig): String { - val key = guid.ifBlank { Utils.getUuid() } - serverStorage?.encode(key, Gson().toJson(config)) - val serverList = decodeServerList() - if (!serverList.contains(key)) { - serverList.add(0, key) - mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList)) - if (mainStorage?.decodeString(KEY_SELECTED_SERVER).isNullOrBlank()) { - mainStorage?.encode(KEY_SELECTED_SERVER, key) - } - } - val profile = ProfileItem( - configType = config.configType, - subscriptionId = config.subscriptionId, - remarks = config.remarks, - server = config.getProxyOutbound()?.getServerAddress(), - serverPort = config.getProxyOutbound()?.getServerPort(), - ) - profileStorage?.encode(key, Gson().toJson(profile)) - return key - } - - fun removeServer(guid: String) { - if (guid.isBlank()) { - return - } - if (mainStorage?.decodeString(KEY_SELECTED_SERVER) == guid) { - mainStorage?.remove(KEY_SELECTED_SERVER) - } - val serverList = decodeServerList() - serverList.remove(guid) - mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList)) - serverStorage?.remove(guid) - profileStorage?.remove(guid) - serverAffStorage?.remove(guid) - } - - fun removeServerViaSubid(subid: String) { - if (subid.isBlank()) { - return - } - serverStorage?.allKeys()?.forEach { key -> - decodeServerConfig(key)?.let { config -> - if (config.subscriptionId == subid) { - removeServer(key) - } - } - } - } - - fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? { - if (guid.isBlank()) { - return null - } - val json = serverAffStorage?.decodeString(guid) - if (json.isNullOrBlank()) { - return null - } - return Gson().fromJson(json, ServerAffiliationInfo::class.java) - } - - fun encodeServerTestDelayMillis(guid: String, testResult: Long) { - if (guid.isBlank()) { - return - } - val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo() - aff.testDelayMillis = testResult - serverAffStorage?.encode(guid, Gson().toJson(aff)) - } - - fun clearAllTestDelayResults(keys: List?) { - keys?.forEach { key -> - decodeServerAffiliationInfo(key)?.let { aff -> - aff.testDelayMillis = 0 - serverAffStorage?.encode(key, Gson().toJson(aff)) - } - } - } - - fun importUrlAsSubscription(url: String): Int { - val subscriptions = decodeSubscriptions() - subscriptions.forEach { - if (it.second.url == url) { - return 0 - } - } - val uri = URI(Utils.fixIllegalUrl(url)) - val subItem = SubscriptionItem() - subItem.remarks = uri.fragment ?: "import sub" - subItem.url = url - subStorage?.encode(Utils.getUuid(), Gson().toJson(subItem)) - return 1 - } - - fun decodeSubscriptions(): List> { - val subscriptions = mutableListOf>() - subStorage?.allKeys()?.forEach { key -> - val json = subStorage?.decodeString(key) - if (!json.isNullOrBlank()) { - subscriptions.add(Pair(key, Gson().fromJson(json, SubscriptionItem::class.java))) - } - } - return subscriptions.sortedBy { (_, value) -> value.addedTime } - } - - fun removeSubscription(subid: String) { - subStorage?.remove(subid) - removeServerViaSubid(subid) - } - - fun decodeAssetUrls(): List> { - val assetUrlItems = mutableListOf>() - assetStorage?.allKeys()?.forEach { key -> - val json = assetStorage?.decodeString(key) - if (!json.isNullOrBlank()) { - assetUrlItems.add(Pair(key, Gson().fromJson(json, AssetUrlItem::class.java))) - } - } - return assetUrlItems.sortedBy { (_, value) -> value.addedTime } - } - - fun removeAssetUrl(assetid: String) { - assetStorage?.remove(assetid) - } - - fun removeAllServer() { - mainStorage?.clearAll() - serverStorage?.clearAll() - profileStorage?.clearAll() - serverAffStorage?.clearAll() - } - - fun removeInvalidServer(guid: String) { - if (guid.isNotEmpty()) { - decodeServerAffiliationInfo(guid)?.let { aff -> - if (aff.testDelayMillis < 0L) { - removeServer(guid) - } - } - } else { - serverAffStorage?.allKeys()?.forEach { key -> - decodeServerAffiliationInfo(key)?.let { aff -> - if (aff.testDelayMillis < 0L) { - removeServer(key) - } - } - } - } - } - - fun sortByTestResults() { - data class ServerDelay(var guid: String, var testDelayMillis: Long) - - val serverDelays = mutableListOf() - val serverList = decodeServerList() - serverList.forEach { key -> - val delay = decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L - serverDelays.add(ServerDelay(key, if (delay <= 0L) 999999 else delay)) - } - serverDelays.sortBy { it.testDelayMillis } - - serverDelays.forEach { - serverList.remove(it.guid) - serverList.add(it.guid) - } - - mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList)) - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt deleted file mode 100644 index c1074430a7..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt +++ /dev/null @@ -1,660 +0,0 @@ -package com.v2ray.ang.util - -import android.content.Context -import android.text.TextUtils -import android.util.Log -import com.google.gson.Gson -import com.tencent.mmkv.MMKV -import com.v2ray.ang.AppConfig -import com.v2ray.ang.AppConfig.ANG_PACKAGE -import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM -import com.v2ray.ang.AppConfig.TAG_BLOCKED -import com.v2ray.ang.AppConfig.TAG_DIRECT -import com.v2ray.ang.AppConfig.TAG_FRAGMENT -import com.v2ray.ang.AppConfig.TAG_PROXY -import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 -import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6 -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ERoutingMode -import com.v2ray.ang.dto.V2rayConfig -import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK -import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP - -object V2rayConfigUtil { - private val serverRawStorage by lazy { - MMKV.mmkvWithID( - MmkvManager.ID_SERVER_RAW, - MMKV.MULTI_PROCESS_MODE - ) - } - private val settingsStorage by lazy { - MMKV.mmkvWithID( - MmkvManager.ID_SETTING, - MMKV.MULTI_PROCESS_MODE - ) - } - - data class Result(var status: Boolean, var content: String) - - /** - * 生成v2ray的客户端配置文件 - */ - fun getV2rayConfig(context: Context, guid: String): Result { - try { - val config = MmkvManager.decodeServerConfig(guid) ?: return Result(false, "") - if (config.configType == EConfigType.CUSTOM) { - val raw = serverRawStorage?.decodeString(guid) - val customConfig = if (raw.isNullOrBlank()) { - config.fullConfig?.toPrettyPrinting() ?: return Result(false, "") - } else { - raw - } - //Log.d(ANG_PACKAGE, customConfig) - return Result(true, customConfig) - } - val outbound = config.getProxyOutbound() ?: return Result(false, "") - val address = outbound.getServerAddress() ?: return Result(false, "") - if (!Utils.isIpAddress(address)) { - if (!Utils.isValidUrl(address)) { - Log.d(ANG_PACKAGE, "$address is an invalid ip or domain") - return Result(false, "") - } - } - - val result = getV2rayNonCustomConfig(context, outbound, config.remarks) - //Log.d(ANG_PACKAGE, result.content) - return result - } catch (e: Exception) { - e.printStackTrace() - return Result(false, "") - } - } - - /** - * 生成v2ray的客户端配置文件 - */ - private fun getV2rayNonCustomConfig( - context: Context, - outbound: V2rayConfig.OutboundBean, - remarks: String, - ): Result { - val result = Result(false, "") - //取得默认配置 - val assets = Utils.readTextFromAssets(context, "v2ray_config.json") - if (TextUtils.isEmpty(assets)) { - return result - } - - //转成Json - val v2rayConfig = Gson().fromJson(assets, V2rayConfig::class.java) ?: return result - - v2rayConfig.log.loglevel = settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL) - ?: "warning" - - inbounds(v2rayConfig) - - updateOutboundWithGlobalSettings(outbound) - v2rayConfig.outbounds[0] = outbound - - updateOutboundFragment(v2rayConfig) - - routing(v2rayConfig) - - fakedns(v2rayConfig) - - dns(v2rayConfig) - - if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { - customLocalDns(v2rayConfig) - } - if (settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) != true) { - v2rayConfig.stats = null - v2rayConfig.policy = null - } - - v2rayConfig.remarks = remarks - - result.status = true - result.content = v2rayConfig.toPrettyPrinting() - return result - } - - /** - * - */ - private fun inbounds(v2rayConfig: V2rayConfig): Boolean { - try { - val socksPort = Utils.parseInt( - settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), - AppConfig.PORT_SOCKS.toInt() - ) - val httpPort = Utils.parseInt( - settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), - AppConfig.PORT_HTTP.toInt() - ) - - v2rayConfig.inbounds.forEach { curInbound -> - if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) { - //bind all inbounds to localhost if the user requests - curInbound.listen = "127.0.0.1" - } - } - v2rayConfig.inbounds[0].port = socksPort - val fakedns = settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) - ?: false - val sniffAllTlsAndHttp = - settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true) - ?: true - v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp - v2rayConfig.inbounds[0].sniffing?.routeOnly = - settingsStorage?.decodeBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false) - if (!sniffAllTlsAndHttp) { - v2rayConfig.inbounds[0].sniffing?.destOverride?.clear() - } - if (fakedns) { - v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns") - } - - v2rayConfig.inbounds[1].port = httpPort - -// if (httpPort > 0) { -// val httpCopy = v2rayConfig.inbounds[0].copy() -// httpCopy.port = httpPort -// httpCopy.protocol = "http" -// v2rayConfig.inbounds.add(httpCopy) -// } - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - private fun fakedns(v2rayConfig: V2rayConfig) { - if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true - && settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true - ) { - v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean()) - } - } - - /** - * routing - */ - private fun routing(v2rayConfig: V2rayConfig): Boolean { - try { - val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) - ?: ERoutingMode.BYPASS_LAN_MAINLAND.value - - routingUserRule( - settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED) - .orEmpty(), TAG_BLOCKED, v2rayConfig - ) - if (routingMode == ERoutingMode.GLOBAL_DIRECT.value) { - routingUserRule( - settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT) - .orEmpty(), TAG_DIRECT, v2rayConfig - ) - routingUserRule( - settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT) - .orEmpty(), TAG_PROXY, v2rayConfig - ) - } else { - routingUserRule( - settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT) - .orEmpty(), TAG_PROXY, v2rayConfig - ) - routingUserRule( - settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT) - .orEmpty(), TAG_DIRECT, v2rayConfig - ) - } - - v2rayConfig.routing.domainStrategy = - settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) - ?: "IPIfNonMatch" - - // Hardcode googleapis.cn gstatic.com - val googleapisRoute = V2rayConfig.RoutingBean.RulesBean( - outboundTag = TAG_PROXY, - domain = arrayListOf("domain:googleapis.cn", "domain:gstatic.com") - ) - - when (routingMode) { - ERoutingMode.BYPASS_LAN.value -> { - routingGeo("", "private", TAG_DIRECT, v2rayConfig) - } - - ERoutingMode.BYPASS_MAINLAND.value -> { - routingGeo("", "cn", TAG_DIRECT, v2rayConfig) - v2rayConfig.routing.rules.add(0, googleapisRoute) - } - - ERoutingMode.BYPASS_LAN_MAINLAND.value -> { - routingGeo("", "private", TAG_DIRECT, v2rayConfig) - routingGeo("", "cn", TAG_DIRECT, v2rayConfig) - v2rayConfig.routing.rules.add(0, googleapisRoute) - } - - ERoutingMode.GLOBAL_DIRECT.value -> { - val globalDirect = V2rayConfig.RoutingBean.RulesBean( - outboundTag = TAG_DIRECT, - ) - if (v2rayConfig.routing.domainStrategy != "IPIfNonMatch") { - globalDirect.port = "0-65535" - } else { - globalDirect.ip = arrayListOf("0.0.0.0/0", "::/0") - } - v2rayConfig.routing.rules.add(globalDirect) - } - } - - if (routingMode != ERoutingMode.GLOBAL_DIRECT.value) { - val globalProxy = V2rayConfig.RoutingBean.RulesBean( - outboundTag = TAG_PROXY, - ) - if (v2rayConfig.routing.domainStrategy != "IPIfNonMatch") { - globalProxy.port = "0-65535" - } else { - globalProxy.ip = arrayListOf("0.0.0.0/0", "::/0") - } - v2rayConfig.routing.rules.add(globalProxy) - } - - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - private fun routingGeo( - ipOrDomain: String, - code: String, - tag: String, - v2rayConfig: V2rayConfig - ) { - try { - if (!TextUtils.isEmpty(code)) { - //IP - if (ipOrDomain == "ip" || ipOrDomain == "") { - val rulesIP = V2rayConfig.RoutingBean.RulesBean() - rulesIP.outboundTag = tag - rulesIP.ip = ArrayList() - rulesIP.ip?.add("geoip:$code") - v2rayConfig.routing.rules.add(rulesIP) - } - - if (ipOrDomain == "domain" || ipOrDomain == "") { - //Domain - val rulesDomain = V2rayConfig.RoutingBean.RulesBean() - rulesDomain.outboundTag = tag - rulesDomain.domain = ArrayList() - rulesDomain.domain?.add("geosite:$code") - v2rayConfig.routing.rules.add(rulesDomain) - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun routingUserRule(userRule: String, tag: String, v2rayConfig: V2rayConfig) { - try { - if (!TextUtils.isEmpty(userRule)) { - //Domain - val rulesDomain = V2rayConfig.RoutingBean.RulesBean() - rulesDomain.outboundTag = tag - rulesDomain.domain = ArrayList() - - //IP - val rulesIP = V2rayConfig.RoutingBean.RulesBean() - rulesIP.outboundTag = tag - rulesIP.ip = ArrayList() - - userRule.split(",").map { it.trim() }.forEach { - if (it.startsWith("ext:") && it.contains("geoip")) { - rulesIP.ip?.add(it) - } else if (Utils.isIpAddress(it) || it.startsWith("geoip:")) { - rulesIP.ip?.add(it) - } else if (it.isNotEmpty()) { - rulesDomain.domain?.add(it) - } - } - if ((rulesDomain.domain?.size ?: 0) > 0) { - v2rayConfig.routing.rules.add(rulesDomain) - } - if ((rulesIP.ip?.size ?: 0) > 0) { - v2rayConfig.routing.rules.add(rulesIP) - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun userRule2Domain(userRule: String): ArrayList { - val domain = ArrayList() - userRule.split(",").map { it.trim() }.forEach { - if (it.startsWith("geosite:") || it.startsWith("domain:")) { - domain.add(it) - } - } - return domain - } - - /** - * Custom Dns - */ - private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean { - try { - if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) { - val geositeCn = arrayListOf("geosite:cn") - val proxyDomain = userRule2Domain( - settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT) - .orEmpty() - ) - val directDomain = userRule2Domain( - settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT) - .orEmpty() - ) - // fakedns with all domains to make it always top priority - v2rayConfig.dns.servers?.add( - 0, - V2rayConfig.DnsBean.ServersBean( - address = "fakedns", - domains = geositeCn.plus(proxyDomain).plus(directDomain) - ) - ) - } - - // DNS inbound对象 - val remoteDns = Utils.getRemoteDnsServers() - if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) { - val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean( - address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else AppConfig.DNS_PROXY, - port = 53, - network = "tcp,udp" - ) - - val localDnsPort = Utils.parseInt( - settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), - AppConfig.PORT_LOCAL_DNS.toInt() - ) - v2rayConfig.inbounds.add( - V2rayConfig.InboundBean( - tag = "dns-in", - port = localDnsPort, - listen = "127.0.0.1", - protocol = "dokodemo-door", - settings = dnsInboundSettings, - sniffing = null - ) - ) - } - - // DNS outbound对象 - if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) { - v2rayConfig.outbounds.add( - V2rayConfig.OutboundBean( - protocol = "dns", - tag = "dns-out", - settings = null, - streamSettings = null, - mux = null - ) - ) - } - - // DNS routing tag - v2rayConfig.routing.rules.add( - 0, V2rayConfig.RoutingBean.RulesBean( - inboundTag = arrayListOf("dns-in"), - outboundTag = "dns-out", - domain = null - ) - ) - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - private fun dns(v2rayConfig: V2rayConfig): Boolean { - try { - val hosts = mutableMapOf() - val servers = ArrayList() - - //remote Dns - val remoteDns = Utils.getRemoteDnsServers() - val proxyDomain = userRule2Domain( - settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT) - .orEmpty() - ) - remoteDns.forEach { - servers.add(it) - } - if (proxyDomain.size > 0) { - servers.add( - V2rayConfig.DnsBean.ServersBean( - remoteDns.first(), - 53, - proxyDomain, - null - ) - ) - } - - // domestic DNS - val domesticDns = Utils.getDomesticDnsServers() - val directDomain = userRule2Domain( - settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT) - .orEmpty() - ) - val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) - ?: ERoutingMode.BYPASS_LAN_MAINLAND.value - val isCnRoutingMode = - (routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) - val geoipCn = arrayListOf("geoip:cn") - - if (directDomain.size > 0) { - servers.add( - V2rayConfig.DnsBean.ServersBean( - domesticDns.first(), - 53, - directDomain, - if (isCnRoutingMode) geoipCn else null - ) - ) - } - if (isCnRoutingMode) { - val geositeCn = arrayListOf("geosite:cn") - servers.add( - V2rayConfig.DnsBean.ServersBean( - domesticDns.first(), - 53, - geositeCn, - geoipCn - ) - ) - } - - if (Utils.isPureIpAddress(domesticDns.first())) { - v2rayConfig.routing.rules.add( - 0, V2rayConfig.RoutingBean.RulesBean( - outboundTag = TAG_DIRECT, - port = "53", - ip = arrayListOf(domesticDns.first()), - domain = null - ) - ) - } - - //block dns - val blkDomain = userRule2Domain( - settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED) - .orEmpty() - ) - if (blkDomain.size > 0) { - hosts.putAll(blkDomain.map { it to "127.0.0.1" }) - } - - // hardcode googleapi rule to fix play store problems - hosts["domain:googleapis.cn"] = "googleapis.com" - - // hardcode popular Android Private DNS rule to fix localhost DNS problem - hosts["dns.pub"] = arrayListOf("1.12.12.12", "120.53.53.53") - hosts["dns.alidns.com"] = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1") - hosts["one.one.one.one"] = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001") - hosts["dns.google"] = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844") - - // DNS dns对象 - v2rayConfig.dns = V2rayConfig.DnsBean( - servers = servers, - hosts = hosts - ) - - // DNS routing - if (Utils.isPureIpAddress(remoteDns.first())) { - v2rayConfig.routing.rules.add( - 0, V2rayConfig.RoutingBean.RulesBean( - outboundTag = TAG_PROXY, - port = "53", - ip = arrayListOf(remoteDns.first()), - domain = null - ) - ) - } - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean { - try { - var muxEnabled = settingsStorage?.decodeBool(AppConfig.PREF_MUX_ENABLED, false) - val protocol = outbound.protocol - if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) - || protocol.equals(EConfigType.SOCKS.name, true) - || protocol.equals(EConfigType.TROJAN.name, true) - || protocol.equals(EConfigType.WIREGUARD.name, true) - ) { - muxEnabled = false - } else if (protocol.equals(EConfigType.VLESS.name, true) - && outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.isNotEmpty() == true - ) { - muxEnabled = false - } - if (muxEnabled == true) { - outbound.mux?.enabled = true - outbound.mux?.concurrency = - settingsStorage?.decodeInt(AppConfig.PREF_MUX_CONCURRENCY) ?: 8 - outbound.mux?.xudpConcurrency = - settingsStorage?.decodeInt(AppConfig.PREF_MUX_XUDP_CONCURRENCY) ?: 8 - outbound.mux?.xudpProxyUDP443 = - settingsStorage?.decodeString(AppConfig.PREF_MUX_XUDP_QUIC) ?: "reject" - } else { - outbound.mux?.enabled = false - outbound.mux?.concurrency = -1 - } - - if (protocol.equals(EConfigType.WIREGUARD.name, true)) { - var localTunAddr = if (outbound.settings?.address == null) { - listOf(WIREGUARD_LOCAL_ADDRESS_V4, WIREGUARD_LOCAL_ADDRESS_V6) - } else { - outbound.settings?.address as List<*> - } - if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) != true) { - localTunAddr = listOf(localTunAddr.first()) - } - outbound.settings?.address = localTunAddr - } - - if (outbound.streamSettings?.network == DEFAULT_NETWORK - && outbound.streamSettings?.tcpSettings?.header?.type == HTTP - ) { - val path = outbound.streamSettings?.tcpSettings?.header?.request?.path - 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 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}""" - } - outbound.streamSettings?.tcpSettings?.header?.request = Gson().fromJson( - requestString, - V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java - ) - outbound.streamSettings?.tcpSettings?.header?.request?.path = - if (path.isNullOrEmpty()) { - listOf("/") - } else { - path - } - outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host - } - - - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean { - try { - if (settingsStorage?.decodeBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) { - return true - } - - - val fragmentOutbound = - V2rayConfig.OutboundBean( - protocol = PROTOCOL_FREEDOM, - tag = TAG_FRAGMENT, - mux = null - ) - - var packets = - settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello" - if (v2rayConfig.outbounds[0].streamSettings?.security == V2rayConfig.REALITY - && packets == "tlshello" - ) { - packets = "1-3" - } else if (v2rayConfig.outbounds[0].streamSettings?.security != V2rayConfig.TLS - && packets == "tlshello" - ) { - packets = "fakehost" - } - - fragmentOutbound.settings = V2rayConfig.OutboundBean.OutSettingsBean( - fragment = V2rayConfig.OutboundBean.OutSettingsBean.FragmentBean( - packets = packets, - length = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_LENGTH) - ?: "10-20", - interval = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL) - ?: "10-20" - ) - ) - fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean( - sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean( - TcpNoDelay = true, - mark = 255 - ) - ) - v2rayConfig.outbounds.add(fragmentOutbound) - - //proxy chain - v2rayConfig.outbounds[0].streamSettings?.sockopt = - V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean( - dialerProxy = TAG_FRAGMENT - ) - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/ShadowsocksFmt.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/ShadowsocksFmt.kt deleted file mode 100644 index 40e068ee07..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/ShadowsocksFmt.kt +++ /dev/null @@ -1,159 +0,0 @@ -package com.v2ray.ang.util.fmt - -import android.util.Log -import com.v2ray.ang.AppConfig -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ServerConfig -import com.v2ray.ang.extension.idnHost -import com.v2ray.ang.util.Utils -import java.net.URI - -object ShadowsocksFmt { - fun parseShadowsocks(str: String): ServerConfig? { - val config = ServerConfig.create(EConfigType.SHADOWSOCKS) - if (!tryResolveResolveSip002(str, config)) { - 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.outboundBean?.settings?.servers?.get(0)?.let { server -> - server.address = match.groupValues[3].removeSurrounding("[", "]") - server.port = match.groupValues[4].toInt() - server.password = match.groupValues[2] - server.method = match.groupValues[1].lowercase() - } - } - return config - } - - fun toUri(config: ServerConfig): String { - val outbound = config.getProxyOutbound() ?: return "" - val remark = "#" + Utils.urlEncode(config.remarks) - val pw = - Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}") - val url = String.format( - "%s@%s:%s", - pw, - Utils.getIpv6Address(outbound.getServerAddress()), - outbound.getServerPort() - ) - return url + remark - } - - private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean { - try { - val uri = URI(Utils.fixIllegalUrl(str)) - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) - - val method: String - val password: String - if (uri.userInfo.contains(":")) { - val arrUserInfo = uri.userInfo.split(":").map { it.trim() } - if (arrUserInfo.count() != 2) { - return false - } - method = arrUserInfo[0] - password = Utils.urlDecode(arrUserInfo[1]) - } else { - val base64Decode = Utils.decode(uri.userInfo) - val arrUserInfo = base64Decode.split(":").map { it.trim() } - if (arrUserInfo.count() < 2) { - return false - } - method = arrUserInfo[0] - password = base64Decode.substringAfter(":") - } - - val query = Utils.urlDecode(uri.query.orEmpty()) - if (query != "") { - val queryPairs = HashMap() - val pairs = query.split(";") - Log.d(AppConfig.ANG_PACKAGE, pairs.toString()) - for (pair in pairs) { - val idx = pair.indexOf("=") - if (idx == -1) { - queryPairs[Utils.urlDecode(pair)] = "" - } else { - queryPairs[Utils.urlDecode(pair.substring(0, idx))] = - Utils.urlDecode(pair.substring(idx + 1)) - } - } - Log.d(AppConfig.ANG_PACKAGE, queryPairs.toString()) - var sni: String? = "" - if (queryPairs["plugin"] == "obfs-local" && queryPairs["obfs"] == "http") { - sni = config.outboundBean?.streamSettings?.populateTransportSettings( - "tcp", - "http", - queryPairs["obfs-host"], - queryPairs["path"], - null, - null, - null, - null, - null, - null - ) - } else if (queryPairs["plugin"] == "v2ray-plugin") { - var network = "ws" - if (queryPairs["mode"] == "quic") { - network = "quic" - } - sni = config.outboundBean?.streamSettings?.populateTransportSettings( - network, - null, - queryPairs["host"], - queryPairs["path"], - null, - null, - null, - null, - null, - null - ) - } - if ("tls" in queryPairs) { - config.outboundBean?.streamSettings?.populateTlsSettings( - "tls", false, sni.orEmpty(), null, null, null, null, null - ) - } - - } - - config.outboundBean?.settings?.servers?.get(0)?.let { server -> - server.address = uri.idnHost - server.port = uri.port - server.password = password - server.method = method - } - return true - } catch (e: Exception) { - Log.d(AppConfig.ANG_PACKAGE, e.toString()) - return false - } - } - -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/SocksFmt.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/SocksFmt.kt deleted file mode 100644 index 0148ec886e..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/SocksFmt.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.v2ray.ang.util.fmt - -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ServerConfig -import com.v2ray.ang.dto.V2rayConfig -import com.v2ray.ang.util.Utils - -object SocksFmt { - fun parseSocks(str: String): ServerConfig? { - val config = ServerConfig.create(EConfigType.SOCKS) - var result = str.replace(EConfigType.SOCKS.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("@") - if (indexS > 0) { - result = Utils.decode(result.substring(0, indexS)) + result.substring( - indexS, - result.length - ) - } else { - result = Utils.decode(result) - } - - val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex() - val match = - legacyPattern.matchEntire(result) ?: return null - - config.outboundBean?.settings?.servers?.get(0)?.let { server -> - server.address = match.groupValues[3].removeSurrounding("[", "]") - server.port = match.groupValues[4].toInt() - val socksUsersBean = - V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() - socksUsersBean.user = match.groupValues[1] - socksUsersBean.pass = match.groupValues[2] - server.users = listOf(socksUsersBean) - } - - return config - } - - fun toUri(config: ServerConfig): String { - val outbound = config.getProxyOutbound() ?: return "" - val remark = "#" + Utils.urlEncode(config.remarks) - val pw = - if (outbound.settings?.servers?.get(0)?.users?.get(0)?.user != null) - "${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}" - else - ":" - val url = String.format( - "%s@%s:%s", - Utils.encode(pw), - Utils.getIpv6Address(outbound.getServerAddress()), - outbound.getServerPort() - ) - return url + remark - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/TrojanFmt.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/TrojanFmt.kt deleted file mode 100644 index c594e364e3..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/TrojanFmt.kt +++ /dev/null @@ -1,180 +0,0 @@ -package com.v2ray.ang.util.fmt - -import android.text.TextUtils -import com.tencent.mmkv.MMKV -import com.v2ray.ang.AppConfig -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ServerConfig -import com.v2ray.ang.dto.V2rayConfig -import com.v2ray.ang.extension.idnHost -import com.v2ray.ang.util.MmkvManager -import com.v2ray.ang.util.Utils -import java.net.URI - -object TrojanFmt { - private val settingsStorage by lazy { - MMKV.mmkvWithID( - MmkvManager.ID_SETTING, - MMKV.MULTI_PROCESS_MODE - ) - } - - fun parseTrojan(str: String): ServerConfig? { - var allowInsecure = !(settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false) - val config = ServerConfig.create(EConfigType.TROJAN) - - val uri = URI(Utils.fixIllegalUrl(str)) - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) - - var flow = "" - var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint - if (uri.rawQuery.isNullOrEmpty()) { - config.outboundBean?.streamSettings?.populateTlsSettings( - V2rayConfig.TLS, - allowInsecure, - "", - fingerprint, - null, - null, - null, - null - ) - } else { - val queryParam = uri.rawQuery.split("&") - .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } - - val sni = config.outboundBean?.streamSettings?.populateTransportSettings( - queryParam["type"] ?: "tcp", - queryParam["headerType"], - queryParam["host"], - queryParam["path"], - queryParam["seed"], - queryParam["quicSecurity"], - queryParam["key"], - queryParam["mode"], - queryParam["serviceName"], - queryParam["authority"] - ) - fingerprint = queryParam["fp"].orEmpty() - allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure - config.outboundBean?.streamSettings?.populateTlsSettings( - queryParam["security"] ?: V2rayConfig.TLS, - allowInsecure, - queryParam["sni"] ?: sni.orEmpty(), - fingerprint, - queryParam["alpn"], - null, - null, - null - ) - flow = queryParam["flow"].orEmpty() - } - config.outboundBean?.settings?.servers?.get(0)?.let { server -> - server.address = uri.idnHost - server.port = uri.port - server.password = uri.userInfo - server.flow = flow - } - - return config - } - - fun toUri(config: ServerConfig): String { - val outbound = config.getProxyOutbound() ?: return "" - val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean() - - val remark = "#" + Utils.urlEncode(config.remarks) - val dicQuery = HashMap() - config.outboundBean?.settings?.servers?.get(0)?.flow?.let { - if (!TextUtils.isEmpty(it)) { - dicQuery["flow"] = it - } - } - - dicQuery["security"] = streamSetting.security.ifEmpty { "none" } - (streamSetting.tlsSettings - ?: streamSetting.realitySettings)?.let { tlsSetting -> - if (!TextUtils.isEmpty(tlsSetting.serverName)) { - dicQuery["sni"] = tlsSetting.serverName - } - if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) { - dicQuery["alpn"] = - Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty() - } - if (!TextUtils.isEmpty(tlsSetting.fingerprint)) { - dicQuery["fp"] = tlsSetting.fingerprint.orEmpty() - } - if (!TextUtils.isEmpty(tlsSetting.publicKey)) { - dicQuery["pbk"] = tlsSetting.publicKey.orEmpty() - } - if (!TextUtils.isEmpty(tlsSetting.shortId)) { - dicQuery["sid"] = tlsSetting.shortId.orEmpty() - } - if (!TextUtils.isEmpty(tlsSetting.spiderX)) { - dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX.orEmpty()) - } - } - dicQuery["type"] = - streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK } - - outbound.getTransportSettingDetails()?.let { transportDetails -> - when (streamSetting.network) { - "tcp" -> { - dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } - if (!TextUtils.isEmpty(transportDetails[1])) { - dicQuery["host"] = Utils.urlEncode(transportDetails[1]) - } - } - - "kcp" -> { - dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } - if (!TextUtils.isEmpty(transportDetails[2])) { - dicQuery["seed"] = Utils.urlEncode(transportDetails[2]) - } - } - - "ws", "httpupgrade", "splithttp" -> { - if (!TextUtils.isEmpty(transportDetails[1])) { - dicQuery["host"] = Utils.urlEncode(transportDetails[1]) - } - if (!TextUtils.isEmpty(transportDetails[2])) { - dicQuery["path"] = Utils.urlEncode(transportDetails[2]) - } - } - - "http", "h2" -> { - dicQuery["type"] = "http" - if (!TextUtils.isEmpty(transportDetails[1])) { - dicQuery["host"] = Utils.urlEncode(transportDetails[1]) - } - if (!TextUtils.isEmpty(transportDetails[2])) { - dicQuery["path"] = Utils.urlEncode(transportDetails[2]) - } - } - - "quic" -> { - dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } - dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1]) - dicQuery["key"] = Utils.urlEncode(transportDetails[2]) - } - - "grpc" -> { - dicQuery["mode"] = transportDetails[0] - dicQuery["authority"] = Utils.urlEncode(transportDetails[1]) - dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2]) - } - } - } - val query = "?" + dicQuery.toList().joinToString( - separator = "&", - transform = { it.first + "=" + it.second }) - - val url = String.format( - "%s@%s:%s", - outbound.getPassword(), - Utils.getIpv6Address(outbound.getServerAddress()), - outbound.getServerPort() - ) - return url + query + remark - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VlessFmt.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VlessFmt.kt deleted file mode 100644 index f540beb6ee..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VlessFmt.kt +++ /dev/null @@ -1,171 +0,0 @@ -package com.v2ray.ang.util.fmt - -import android.text.TextUtils -import com.tencent.mmkv.MMKV -import com.v2ray.ang.AppConfig -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ServerConfig -import com.v2ray.ang.dto.V2rayConfig -import com.v2ray.ang.extension.idnHost -import com.v2ray.ang.util.MmkvManager -import com.v2ray.ang.util.Utils -import java.net.URI - -object VlessFmt { - private val settingsStorage by lazy { - MMKV.mmkvWithID( - MmkvManager.ID_SETTING, - MMKV.MULTI_PROCESS_MODE - ) - } - - fun parseVless(str: String): ServerConfig? { - var allowInsecure = !(settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false) - val config = ServerConfig.create(EConfigType.VLESS) - - val uri = URI(Utils.fixIllegalUrl(str)) - if (uri.rawQuery.isNullOrEmpty()) return null - val queryParam = uri.rawQuery.split("&") - .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } - - val streamSetting = config.outboundBean?.streamSettings ?: return null - - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) - config.outboundBean.settings?.vnext?.get(0)?.let { vnext -> - vnext.address = uri.idnHost - vnext.port = uri.port - vnext.users[0].id = uri.userInfo - vnext.users[0].encryption = queryParam["encryption"] ?: "none" - vnext.users[0].flow = queryParam["flow"].orEmpty() - } - - val sni = streamSetting.populateTransportSettings( - queryParam["type"] ?: "tcp", - queryParam["headerType"], - queryParam["host"], - queryParam["path"], - queryParam["seed"], - queryParam["quicSecurity"], - queryParam["key"], - queryParam["mode"], - queryParam["serviceName"], - queryParam["authority"] - ) - allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure - streamSetting.populateTlsSettings( - queryParam["security"].orEmpty(), - allowInsecure, - queryParam["sni"] ?: sni, - queryParam["fp"].orEmpty(), - queryParam["alpn"], - queryParam["pbk"].orEmpty(), - queryParam["sid"].orEmpty(), - queryParam["spx"].orEmpty() - ) - - return config - } - - fun toUri(config: ServerConfig): String { - val outbound = config.getProxyOutbound() ?: return "" - val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean() - - val remark = "#" + Utils.urlEncode(config.remarks) - val dicQuery = HashMap() - outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let { - if (!TextUtils.isEmpty(it)) { - dicQuery["flow"] = it - } - } - dicQuery["encryption"] = - if (outbound.getSecurityEncryption().isNullOrEmpty()) "none" - else outbound.getSecurityEncryption().orEmpty() - - - dicQuery["security"] = streamSetting.security.ifEmpty { "none" } - (streamSetting.tlsSettings - ?: streamSetting.realitySettings)?.let { tlsSetting -> - if (!TextUtils.isEmpty(tlsSetting.serverName)) { - dicQuery["sni"] = tlsSetting.serverName - } - if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) { - dicQuery["alpn"] = - Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty() - } - if (!TextUtils.isEmpty(tlsSetting.fingerprint)) { - dicQuery["fp"] = tlsSetting.fingerprint.orEmpty() - } - if (!TextUtils.isEmpty(tlsSetting.publicKey)) { - dicQuery["pbk"] = tlsSetting.publicKey.orEmpty() - } - if (!TextUtils.isEmpty(tlsSetting.shortId)) { - dicQuery["sid"] = tlsSetting.shortId.orEmpty() - } - if (!TextUtils.isEmpty(tlsSetting.spiderX)) { - dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX.orEmpty()) - } - } - dicQuery["type"] = - streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK } - - outbound.getTransportSettingDetails()?.let { transportDetails -> - when (streamSetting.network) { - "tcp" -> { - dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } - if (!TextUtils.isEmpty(transportDetails[1])) { - dicQuery["host"] = Utils.urlEncode(transportDetails[1]) - } - } - - "kcp" -> { - dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } - if (!TextUtils.isEmpty(transportDetails[2])) { - dicQuery["seed"] = Utils.urlEncode(transportDetails[2]) - } - } - - "ws", "httpupgrade", "splithttp" -> { - if (!TextUtils.isEmpty(transportDetails[1])) { - dicQuery["host"] = Utils.urlEncode(transportDetails[1]) - } - if (!TextUtils.isEmpty(transportDetails[2])) { - dicQuery["path"] = Utils.urlEncode(transportDetails[2]) - } - } - - "http", "h2" -> { - dicQuery["type"] = "http" - if (!TextUtils.isEmpty(transportDetails[1])) { - dicQuery["host"] = Utils.urlEncode(transportDetails[1]) - } - if (!TextUtils.isEmpty(transportDetails[2])) { - dicQuery["path"] = Utils.urlEncode(transportDetails[2]) - } - } - - "quic" -> { - dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" } - dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1]) - dicQuery["key"] = Utils.urlEncode(transportDetails[2]) - } - - "grpc" -> { - dicQuery["mode"] = transportDetails[0] - dicQuery["authority"] = Utils.urlEncode(transportDetails[1]) - dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2]) - } - } - } - val query = "?" + dicQuery.toList().joinToString( - separator = "&", - transform = { it.first + "=" + it.second }) - - val url = String.format( - "%s@%s:%s", - outbound.getPassword(), - Utils.getIpv6Address(outbound.getServerAddress()), - outbound.getServerPort() - ) - return url + query + remark - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VmessFmt.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VmessFmt.kt deleted file mode 100644 index 41fb8f1f77..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VmessFmt.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.v2ray.ang.util.fmt - -import android.text.TextUtils -import android.util.Log -import com.google.gson.Gson -import com.tencent.mmkv.MMKV -import com.v2ray.ang.AppConfig -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ServerConfig -import com.v2ray.ang.dto.V2rayConfig -import com.v2ray.ang.dto.VmessQRCode -import com.v2ray.ang.extension.idnHost -import com.v2ray.ang.util.MmkvManager -import com.v2ray.ang.util.Utils -import java.net.URI - -object VmessFmt { - private val settingsStorage by lazy { - MMKV.mmkvWithID( - MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE - ) - } - - fun parseVmess(str: String): ServerConfig? { - if (str.indexOf('?') > 0 && str.indexOf('&') > 0) { - return parseVmessStd(str) - } - - val allowInsecure = !(settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false) - val config = ServerConfig.create(EConfigType.VMESS) - val streamSetting = config.outboundBean?.streamSettings ?: return null - var result = str.replace(EConfigType.VMESS.protocolScheme, "") - result = Utils.decode(result) - if (TextUtils.isEmpty(result)) { - Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed") - return null - } - val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java) - // Although VmessQRCode fields are non null, looks like Gson may still create null fields - if (TextUtils.isEmpty(vmessQRCode.add) - || TextUtils.isEmpty(vmessQRCode.port) - || TextUtils.isEmpty(vmessQRCode.id) - || TextUtils.isEmpty(vmessQRCode.net) - ) { - Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol") - return null - } - - config.remarks = vmessQRCode.ps - config.outboundBean.settings?.vnext?.get(0)?.let { vnext -> - vnext.address = vmessQRCode.add - vnext.port = Utils.parseInt(vmessQRCode.port) - vnext.users[0].id = vmessQRCode.id - vnext.users[0].security = - if (TextUtils.isEmpty(vmessQRCode.scy)) V2rayConfig.DEFAULT_SECURITY else vmessQRCode.scy - vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid) - } - val sni = streamSetting.populateTransportSettings( - vmessQRCode.net, - vmessQRCode.type, - vmessQRCode.host, - vmessQRCode.path, - vmessQRCode.path, - vmessQRCode.host, - vmessQRCode.path, - vmessQRCode.type, - vmessQRCode.path, - vmessQRCode.host - ) - - val fingerprint = vmessQRCode.fp - streamSetting.populateTlsSettings( - vmessQRCode.tls, - allowInsecure, - if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni, - fingerprint, - vmessQRCode.alpn, - null, - null, - null - ) - - return config - } - - fun toUri(config: ServerConfig): String { - val outbound = config.getProxyOutbound() ?: return "" - val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean() - - val vmessQRCode = VmessQRCode() - vmessQRCode.v = "2" - vmessQRCode.ps = config.remarks - vmessQRCode.add = outbound.getServerAddress().orEmpty() - vmessQRCode.port = outbound.getServerPort().toString() - vmessQRCode.id = outbound.getPassword().orEmpty() - vmessQRCode.aid = outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString() - vmessQRCode.scy = outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString() - vmessQRCode.net = streamSetting.network - vmessQRCode.tls = streamSetting.security - vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty() - vmessQRCode.alpn = - Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString()).orEmpty() - vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty() - outbound.getTransportSettingDetails()?.let { transportDetails -> - vmessQRCode.type = transportDetails[0] - vmessQRCode.host = transportDetails[1] - vmessQRCode.path = transportDetails[2] - } - val json = Gson().toJson(vmessQRCode) - return Utils.encode(json) - } - - fun parseVmessStd(str: String): ServerConfig? { - var allowInsecure = !(settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false) - val config = ServerConfig.create(EConfigType.VMESS) - - val uri = URI(Utils.fixIllegalUrl(str)) - if (uri.rawQuery.isNullOrEmpty()) return null - val queryParam = uri.rawQuery.split("&") - .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } - - val streamSetting = config.outboundBean?.streamSettings ?: return null - - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) - config.outboundBean.settings?.vnext?.get(0)?.let { vnext -> - vnext.address = uri.idnHost - vnext.port = uri.port - vnext.users[0].id = uri.userInfo - vnext.users[0].security = V2rayConfig.DEFAULT_SECURITY - vnext.users[0].alterId = 0 - } - - val sni = streamSetting.populateTransportSettings( - queryParam["type"] ?: "tcp", - queryParam["headerType"], - queryParam["host"], - queryParam["path"], - queryParam["seed"], - queryParam["quicSecurity"], - queryParam["key"], - queryParam["mode"], - queryParam["serviceName"], - queryParam["authority"] - ) - - allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure - streamSetting.populateTlsSettings( - queryParam["security"].orEmpty(), - allowInsecure, - queryParam["sni"] ?: sni, - queryParam["fp"].orEmpty(), - queryParam["alpn"], - null, - null, - null - ) - - return config - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/WireguardFmt.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/WireguardFmt.kt deleted file mode 100644 index c1273a90a0..0000000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/WireguardFmt.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.v2ray.ang.util.fmt - -import com.v2ray.ang.AppConfig -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ServerConfig -import com.v2ray.ang.extension.idnHost -import com.v2ray.ang.extension.removeWhiteSpace -import com.v2ray.ang.util.Utils -import java.net.URI - -object WireguardFmt { - fun parseWireguard(str: String): ServerConfig? { - val uri = URI(Utils.fixIllegalUrl(str)) - if (uri.rawQuery != null) { - val config = ServerConfig.create(EConfigType.WIREGUARD) - config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) - - val queryParam = uri.rawQuery.split("&") - .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } - - config.outboundBean?.settings?.let { wireguard -> - wireguard.secretKey = uri.userInfo - wireguard.address = - (queryParam["address"] - ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace() - .split(",") - wireguard.peers?.get(0)?.publicKey = queryParam["publickey"].orEmpty() - wireguard.peers?.get(0)?.endpoint = - Utils.getIpv6Address(uri.idnHost) + ":${uri.port}" - wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU) - wireguard.reserved = - (queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",") - .map { it.toInt() } - wireguard.peers?.get(0)?.keepAlive = Utils.parseInt(queryParam["keepalive"] ?: AppConfig.WIREGUARD_keep_alive) - wireguard.wnoise = queryParam["wnoise"] ?: AppConfig.WIREGUARD_wnoise - wireguard.wnoisecount = queryParam["wnoisecount"] ?: AppConfig.WIREGUARD_wnoisecount - wireguard.wnoisedelay = queryParam["wnoisedelay"] ?: AppConfig.WIREGUARD_wnoisedelay - wireguard.wpayloadsize = queryParam["wpayloadsize"] ?: AppConfig.WIREGUARD_wpayloadsize - } - return config - } else { - return null - } - } - - fun toUri(config: ServerConfig): String { - val outbound = config.getProxyOutbound() ?: return "" - - val remark = "#" + Utils.urlEncode(config.remarks) - val dicQuery = HashMap() - dicQuery["publickey"] = - Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString()) - if (outbound.settings?.reserved != null) { - dicQuery["reserved"] = Utils.urlEncode( - Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString()) - .toString() - ) - } - dicQuery["address"] = Utils.urlEncode( - Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString()) - .toString() - ) - if (outbound.settings?.mtu != null) { - dicQuery["mtu"] = outbound.settings?.mtu.toString() - } - - if (outbound.settings?.peers?.get(0)?.keepAlive != null) { - dicQuery["keepalive"] = outbound.settings?.peers?.get(0)?.keepAlive.toString() - } - - if (outbound.settings?.wnoise != null) { - dicQuery["wnoise"] = outbound.settings?.wnoise.toString() - } - if (outbound.settings?.wnoisecount != null) { - dicQuery["wnoisecount"] = outbound.settings?.wnoisecount.toString() - } - if (outbound.settings?.wnoisedelay != null) { - dicQuery["wnoisedelay"] = outbound.settings?.wnoisedelay.toString() - } - if (outbound.settings?.wpayloadsize != null) { - dicQuery["wpayloadsize"] = outbound.settings?.wpayloadsize.toString() - } - - val query = "?" + dicQuery.toList().joinToString( - separator = "&", - transform = { it.first + "=" + it.second }) - - val url = String.format( - "%s@%s:%s", - Utils.urlEncode(outbound.getPassword().toString()), - Utils.getIpv6Address(outbound.getServerAddress()), - outbound.getServerPort() - ) - return url + query + remark - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_lock_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_lock_24dp.xml new file mode 100644 index 0000000000..48309ad9cf --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_lock_24dp.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_routing_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_routing_24dp.xml new file mode 100644 index 0000000000..7002c085b9 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_routing_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_lock_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_lock_24dp.xml new file mode 100644 index 0000000000..da0e84b493 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_lock_24dp.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_routing_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_routing_24dp.xml new file mode 100644 index 0000000000..9032a2e676 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_routing_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/license_24px.xml b/V2rayNG/app/src/main/res/drawable/license_24px.xml new file mode 100644 index 0000000000..99b9354b0b --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/license_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_about.xml b/V2rayNG/app/src/main/res/layout/activity_about.xml index 640192eef0..15262fd6ac 100644 --- a/V2rayNG/app/src/main/res/layout/activity_about.xml +++ b/V2rayNG/app/src/main/res/layout/activity_about.xml @@ -1,7 +1,8 @@ + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + android:padding="@dimen/padding"> + android:paddingStart="@dimen/padding_start"> @@ -58,12 +59,12 @@ android:id="@+id/layout_share" android:layout_width="match_parent" android:layout_height="@dimen/server_height" + android:background="?attr/selectableItemBackground" android:clickable="true" android:focusable="true" - android:background="?attr/selectableItemBackground" android:gravity="center|start" android:orientation="horizontal" - android:padding="16dp"> + android:padding="@dimen/padding"> @@ -82,12 +83,12 @@ android:id="@+id/layout_restore" android:layout_width="match_parent" android:layout_height="@dimen/server_height" + android:background="?attr/selectableItemBackground" android:clickable="true" android:focusable="true" - android:background="?attr/selectableItemBackground" android:gravity="center|start" android:orientation="horizontal" - android:padding="16dp"> + android:padding="@dimen/padding"> @@ -108,18 +109,18 @@ android:layout_height="match_parent" android:gravity="top" android:orientation="vertical" - android:paddingTop="16dp"> + android:paddingTop="@dimen/padding_start"> + android:padding="@dimen/padding"> + + + + + + + + android:padding="@dimen/padding"> @@ -163,12 +188,12 @@ android:id="@+id/layout_tg_channel" android:layout_width="match_parent" android:layout_height="@dimen/server_height" + android:background="?attr/selectableItemBackground" android:clickable="true" android:focusable="true" - android:background="?attr/selectableItemBackground" android:gravity="center|start" android:orientation="horizontal" - android:padding="16dp"> + android:padding="@dimen/padding"> @@ -187,12 +212,12 @@ android:id="@+id/layout_privacy_policy" android:layout_width="match_parent" android:layout_height="@dimen/server_height" + android:background="?attr/selectableItemBackground" android:clickable="true" android:focusable="true" - android:background="?attr/selectableItemBackground" android:gravity="center|start" android:orientation="horizontal" - android:padding="16dp"> + android:padding="@dimen/padding"> @@ -212,7 +237,7 @@ android:layout_height="@dimen/server_height" android:gravity="center" android:orientation="horizontal" - android:padding="16dp"> + android:padding="@dimen/padding"> + android:paddingStart="@dimen/padding_start" + android:paddingEnd="@dimen/padding_end"> - - + android:maxLines="2" + android:text="@string/title_pref_per_app_proxy" + android:textAppearance="@style/TextAppearance.AppCompat.Small" + app:theme="@style/BrandedSwitch" /> @@ -56,18 +54,13 @@ android:gravity="center" android:orientation="horizontal"> - - + android:text="@string/switch_bypass_apps_mode" + android:textAppearance="@style/TextAppearance.AppCompat.Small" + app:theme="@style/BrandedSwitch" /> @@ -84,6 +77,7 @@ - + android:layout_height="match_parent"> - - - - + android:layout_height="match_parent" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_main.xml b/V2rayNG/app/src/main/res/layout/activity_main.xml index 7c5ff6e3fe..072e36f92d 100644 --- a/V2rayNG/app/src/main/res/layout/activity_main.xml +++ b/V2rayNG/app/src/main/res/layout/activity_main.xml @@ -3,12 +3,12 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/drawer_layout" android:layout_width="match_parent" - android:layout_height="match_parent" - android:fitsSystemWindows="true"> + android:layout_height="match_parent"> + android:layout_height="match_parent"> + android:visibility="invisible" + app:indicatorColor="@color/color_fab_active" /> + app:tabIndicatorFullWidth="false" + app:tabMode="scrollable" + app:tabTextAppearance="@style/TabLayoutTextStyle" /> + android:gravity="center|start" + android:nextFocusLeft="@+id/recycler_view" + android:nextFocusRight="@+id/fab"> @@ -89,7 +90,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" - android:layout_marginBottom="24dp"> + android:layout_marginBottom="12dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_routing_setting.xml b/V2rayNG/app/src/main/res/layout/activity_routing_setting.xml new file mode 100644 index 0000000000..825c1c507d --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_routing_setting.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_routing_settings.xml b/V2rayNG/app/src/main/res/layout/activity_routing_settings.xml deleted file mode 100644 index a77a806205..0000000000 --- a/V2rayNG/app/src/main/res/layout/activity_routing_settings.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/V2rayNG/app/src/main/res/layout/activity_server_custom_config.xml b/V2rayNG/app/src/main/res/layout/activity_server_custom_config.xml index e1e140e764..40423b5419 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_custom_config.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_custom_config.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".ui.ServerCustomConfigActivity"> + android:layout_margin="@dimen/layout_margin_spacing" + android:orientation="vertical"> + android:layout_margin="@dimen/layout_margin_spacing" + android:orientation="vertical"> + android:layout_marginTop="@dimen/layout_margin_top_height" + android:gravity="top|start" /> diff --git a/V2rayNG/app/src/main/res/layout/activity_server_hysteria2.xml b/V2rayNG/app/src/main/res/layout/activity_server_hysteria2.xml new file mode 100644 index 0000000000..a0228555b5 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_server_hysteria2.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml b/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml index 926a09f3ed..83dbfc9695 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".ui.ServerActivity"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/V2rayNG/app/src/main/res/layout/activity_server_socks.xml b/V2rayNG/app/src/main/res/layout/activity_server_socks.xml index 5f34c37cdf..42f62a3f22 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_socks.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_socks.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".ui.ServerActivity"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + - diff --git a/V2rayNG/app/src/main/res/layout/activity_server_trojan.xml b/V2rayNG/app/src/main/res/layout/activity_server_trojan.xml index 9f4ba2e411..b98753d0be 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_trojan.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_trojan.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".ui.ServerActivity"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - diff --git a/V2rayNG/app/src/main/res/layout/activity_server_vless.xml b/V2rayNG/app/src/main/res/layout/activity_server_vless.xml index 11a1c801a6..c7bf624b42 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_vless.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_vless.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".ui.ServerActivity"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml b/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml index 55e70e5a69..413127e408 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".ui.ServerActivity"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/V2rayNG/app/src/main/res/layout/activity_server_wireguard.xml b/V2rayNG/app/src/main/res/layout/activity_server_wireguard.xml index 83c57a4d40..a43eeffb02 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_wireguard.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_wireguard.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".ui.ServerActivity"> - - - - + - - - - - - - - + android:text="@string/server_lab_secret_key" /> @@ -70,34 +42,16 @@ - - - - - + android:text="@string/server_lab_public_key" /> - - + android:text="@string/server_lab_preshared_key" /> @@ -136,21 +90,10 @@ + android:inputType="text" /> - - - @@ -200,6 +143,7 @@ + android:text="wnoise (any HEX,quic,random,none)" /> @@ -244,6 +189,7 @@ android:id="@+id/et_wnoisecount" android:layout_width="match_parent" android:layout_height="@dimen/edit_height" + android:hint="15" android:inputType="text" /> @@ -263,6 +209,7 @@ android:id="@+id/et_wnoisedelay" android:layout_width="match_parent" android:layout_height="@dimen/edit_height" + android:hint="1-2" android:inputType="text" /> @@ -282,6 +229,7 @@ android:id="@+id/et_wpayloadsize" android:layout_width="match_parent" android:layout_height="@dimen/edit_height" + android:hint="5-10" android:inputType="text" /> @@ -289,8 +237,8 @@ diff --git a/V2rayNG/app/src/main/res/layout/activity_settings.xml b/V2rayNG/app/src/main/res/layout/activity_settings.xml index f674f2ae03..618bc8c0ef 100644 --- a/V2rayNG/app/src/main/res/layout/activity_settings.xml +++ b/V2rayNG/app/src/main/res/layout/activity_settings.xml @@ -1,8 +1,16 @@ - \ No newline at end of file + android:fitsSystemWindows="true" + android:orientation="vertical" + tools:context=".ui.SettingsActivity"> + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml b/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml index ee30498952..504c1e076f 100644 --- a/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml +++ b/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml @@ -3,6 +3,8 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:fitsSystemWindows="true" tools:context=".ui.SubSettingActivity"> + android:orientation="vertical"> + + + + + + + + + + + + + + + + + - - + android:paddingStart="@dimen/padding_start" + android:paddingEnd="@dimen/padding_end" + app:theme="@style/BrandedSwitch" /> @@ -82,12 +128,13 @@ android:text="@string/sub_auto_update" /> - + android:paddingStart="@dimen/padding_start" + android:paddingEnd="@dimen/padding_end" + app:theme="@style/BrandedSwitch" /> @@ -98,32 +145,45 @@ android:orientation="vertical"> - + android:text="@string/sub_setting_pre_profile" /> + android:layout_height="@dimen/edit_height" + android:hint="@string/sub_setting_pre_profile_tip" + android:inputType="text" /> + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_tasker.xml b/V2rayNG/app/src/main/res/layout/activity_tasker.xml index 5c798c3765..6fb211b162 100644 --- a/V2rayNG/app/src/main/res/layout/activity_tasker.xml +++ b/V2rayNG/app/src/main/res/layout/activity_tasker.xml @@ -1,9 +1,11 @@ + android:checked="true" + app:theme="@style/BrandedSwitch" /> diff --git a/V2rayNG/app/src/main/res/layout/activity_user_asset_url.xml b/V2rayNG/app/src/main/res/layout/activity_user_asset_url.xml index 15247c42e1..d46391c0ac 100644 --- a/V2rayNG/app/src/main/res/layout/activity_user_asset_url.xml +++ b/V2rayNG/app/src/main/res/layout/activity_user_asset_url.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".ui.UserAssetUrlActivity"> @@ -78,8 +78,8 @@ diff --git a/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml b/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml deleted file mode 100644 index 748abd49b5..0000000000 --- a/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_bypass_list.xml b/V2rayNG/app/src/main/res/layout/item_recycler_bypass_list.xml index d43866c0b7..f074f9ff56 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_bypass_list.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_bypass_list.xml @@ -11,10 +11,8 @@ android:id="@+id/icon" android:layout_width="60dp" android:layout_height="60dp" - android:paddingStart="10dp" - android:paddingLeft="10dp" - android:paddingEnd="10dp" - android:paddingRight="10dp" /> + android:paddingStart="@dimen/padding_start" + android:paddingEnd="@dimen/padding_end" /> + android:paddingStart="@dimen/padding_start" + android:paddingEnd="@dimen/padding_end"> + android:paddingStart="@dimen/padding_start" /> \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml b/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml index 45f9cd949d..6c4df973c8 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml @@ -12,7 +12,7 @@ android:layout_gravity="center" android:gravity="center" android:orientation="horizontal" - android:padding="5dp"> + android:padding="@dimen/padding"> + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_main.xml b/V2rayNG/app/src/main/res/layout/item_recycler_main.xml index 157a160480..ac27a40e4e 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_main.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_main.xml @@ -11,7 +11,7 @@ android:id="@+id/card_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="1dp" + android:layout_margin="@dimen/cardview_margin" app:cardCornerRadius="5dp"> + android:paddingStart="@dimen/padding_start"> + android:paddingEnd="@dimen/padding_end"> + android:paddingEnd="@dimen/padding_end"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml b/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml index 8589b0c5fd..663c53ab43 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml @@ -10,35 +10,28 @@ android:id="@+id/item_cardview" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="1dp" + android:layout_margin="@dimen/cardview_margin" android:orientation="horizontal" card_view:cardCornerRadius="5dp"> - - + android:paddingStart="@dimen/padding_start"> - + android:orientation="vertical" + android:paddingStart="@dimen/padding_start" + android:paddingEnd="@dimen/padding_end"> - - - + android:orientation="horizontal"> + + + + + + + + + + + + - - - + android:layout_marginTop="@dimen/layout_margin_top_height" + android:orientation="horizontal"> + + - diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml b/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml index 397f168186..cf1bf5976b 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml @@ -8,7 +8,7 @@ + android:paddingStart="@dimen/padding_start"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/layout_progress.xml b/V2rayNG/app/src/main/res/layout/layout_progress.xml index 8c69942038..8e431cad0b 100644 --- a/V2rayNG/app/src/main/res/layout/layout_progress.xml +++ b/V2rayNG/app/src/main/res/layout/layout_progress.xml @@ -5,7 +5,7 @@ android:layout_height="match_parent" android:gravity="center_vertical" android:orientation="horizontal" - android:padding="16dp"> + android:padding="@dimen/padding"> @@ -109,7 +109,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/layout_transport.xml b/V2rayNG/app/src/main/res/layout/layout_transport.xml new file mode 100644 index 0000000000..bda1e11c2c --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/layout_transport.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/nav_header.xml b/V2rayNG/app/src/main/res/layout/nav_header.xml index 575897e552..198767e4b1 100644 --- a/V2rayNG/app/src/main/res/layout/nav_header.xml +++ b/V2rayNG/app/src/main/res/layout/nav_header.xml @@ -5,9 +5,6 @@ android:background="@drawable/nikamenu1" android:gravity="center" android:orientation="vertical" - android:paddingLeft="@dimen/activity_horizontal_margin" - android:paddingTop="@dimen/activity_vertical_margin" - android:paddingRight="@dimen/activity_horizontal_margin" - android:paddingBottom="@dimen/activity_vertical_margin"> + android:padding="@dimen/activity_horizontal_margin"> diff --git a/V2rayNG/app/src/main/res/layout/widget_switch.xml b/V2rayNG/app/src/main/res/layout/widget_switch.xml index 71601cc6ce..bf0bfc71de 100644 --- a/V2rayNG/app/src/main/res/layout/widget_switch.xml +++ b/V2rayNG/app/src/main/res/layout/widget_switch.xml @@ -18,7 +18,7 @@ android:id="@+id/image_switch" android:layout_width="45dp" android:layout_height="45dp" - android:padding="12dp" + android:padding="@dimen/padding" app:srcCompat="@drawable/ic_stat_name" /> diff --git a/V2rayNG/app/src/main/res/menu/action_server.xml b/V2rayNG/app/src/main/res/menu/action_server.xml index e0b34e3990..62785bcaf7 100644 --- a/V2rayNG/app/src/main/res/menu/action_server.xml +++ b/V2rayNG/app/src/main/res/menu/action_server.xml @@ -5,10 +5,10 @@ android:id="@+id/del_config" android:icon="@drawable/ic_delete_24dp" android:title="@string/menu_item_del_config" - app:showAsAction="always" /> + app:showAsAction="ifRoom" /> + app:showAsAction="ifRoom" /> \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/action_sub_setting.xml b/V2rayNG/app/src/main/res/menu/action_sub_setting.xml index 3179cb1a5e..fa77aa07ef 100644 --- a/V2rayNG/app/src/main/res/menu/action_sub_setting.xml +++ b/V2rayNG/app/src/main/res/menu/action_sub_setting.xml @@ -5,10 +5,10 @@ android:id="@+id/add_config" android:icon="@drawable/ic_add_24dp" android:title="@string/menu_item_add_config" - app:showAsAction="always" /> + app:showAsAction="ifRoom" /> + app:showAsAction="ifRoom" /> \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/menu_asset.xml b/V2rayNG/app/src/main/res/menu/menu_asset.xml index 72fa7c20d4..fc351ece4e 100644 --- a/V2rayNG/app/src/main/res/menu/menu_asset.xml +++ b/V2rayNG/app/src/main/res/menu/menu_asset.xml @@ -14,11 +14,15 @@ android:id="@+id/add_url" android:title="@string/menu_item_add_url" app:showAsAction="never" /> + + app:showAsAction="ifRoom" /> \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/menu_bypass_list.xml b/V2rayNG/app/src/main/res/menu/menu_bypass_list.xml index 63fed36e22..8d9502089a 100644 --- a/V2rayNG/app/src/main/res/menu/menu_bypass_list.xml +++ b/V2rayNG/app/src/main/res/menu/menu_bypass_list.xml @@ -6,7 +6,7 @@ android:icon="@drawable/ic_description_24dp" android:title="@string/menu_item_search" app:actionViewClass="androidx.appcompat.widget.SearchView" - app:showAsAction="always" /> + app:showAsAction="ifRoom" /> + android:id="@+id/routing_setting" + android:icon="@drawable/ic_routing_24dp" + android:title="@string/routing_settings_title" /> diff --git a/V2rayNG/app/src/main/res/menu/menu_logcat.xml b/V2rayNG/app/src/main/res/menu/menu_logcat.xml index 8044bbde99..14911e578e 100644 --- a/V2rayNG/app/src/main/res/menu/menu_logcat.xml +++ b/V2rayNG/app/src/main/res/menu/menu_logcat.xml @@ -1,14 +1,20 @@ + + app:showAsAction="ifRoom" /> + app:showAsAction="ifRoom" /> \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/menu_main.xml b/V2rayNG/app/src/main/res/menu/menu_main.xml index 7894237ba1..ce4b16a497 100644 --- a/V2rayNG/app/src/main/res/menu/menu_main.xml +++ b/V2rayNG/app/src/main/res/menu/menu_main.xml @@ -36,6 +36,10 @@ android:id="@+id/import_manually_socks" android:title="@string/menu_item_import_config_manually_socks" app:showAsAction="never" /> + + + app:showAsAction="ifRoom"> - - - - - - - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/menu_routing_setting.xml b/V2rayNG/app/src/main/res/menu/menu_routing_setting.xml new file mode 100644 index 0000000000..86a9c2f8dd --- /dev/null +++ b/V2rayNG/app/src/main/res/menu/menu_routing_setting.xml @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/menu/menu_scanner.xml b/V2rayNG/app/src/main/res/menu/menu_scanner.xml index 5a51ffc749..9cd25c2f7f 100644 --- a/V2rayNG/app/src/main/res/menu/menu_scanner.xml +++ b/V2rayNG/app/src/main/res/menu/menu_scanner.xml @@ -5,10 +5,10 @@ android:id="@+id/scan_code" android:icon="@drawable/ic_scan_24dp" android:title="" - app:showAsAction="always" /> + app:showAsAction="ifRoom" /> + app:showAsAction="ifRoom" /> \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/raw/licenses.xml b/V2rayNG/app/src/main/res/raw/licenses.xml index 067233426e..0ab99d8bcb 100644 --- a/V2rayNG/app/src/main/res/raw/licenses.xml +++ b/V2rayNG/app/src/main/res/raw/licenses.xml @@ -29,12 +29,6 @@ Copyright 2010-2016 JetBrains s.r.o. Apache Software License 2.0 - - Logger - https://github.com/orhanobut/logger - Copyright 2015 Orhan Obut - Apache Software License 2.0 - LeakCanary https://github.com/square/leakcanary diff --git a/V2rayNG/app/src/main/res/values-ar/strings.xml b/V2rayNG/app/src/main/res/values-ar/strings.xml index 350edb64a0..c4ba1c9fe1 100644 --- a/V2rayNG/app/src/main/res/values-ar/strings.xml +++ b/V2rayNG/app/src/main/res/values-ar/strings.xml @@ -8,10 +8,12 @@ إغلاق درج التنقل نجحت عملية ترحيل البيانات! فشلت عملية ترحيل البيانات! + Please pull down to refresh! إيقاف تعذر الحصول على الإذن + Unable to obtain the notification permission انقر للمزيد بدء الخدمات إيقاف الخدمات @@ -25,12 +27,14 @@ حذف التكوين استيراد التكوين من رمز الاستجابة السريعة (QRcode) استيراد التكوين من الحافظة - الكتابة يدويًا [Vmess] + الكتابة يدويًا [VMess] الكتابة يدويًا [VLESS] الكتابة يدويًا [Shadowsocks] - الكتابة يدويًا [Socks] + الكتابة يدويًا [SOCKS] + Type manually[HTTP] الكتابة يدويًا [Trojan] الكتابة يدويًا [Wireguard] + Type manually[Hysteria2] تكوين مخصص استيراد تكوين مخصص من الحافظة استيراد تكوين مخصص من الجهاز @@ -53,14 +57,14 @@ http host ws host httpupgrade host - splithttp host + xhttp host h2 host QUIC security gRPC Authority path ws path httpupgrade path - splithttp path + xhttp path h2 path QUIC key kcp seed @@ -79,12 +83,13 @@ التشفير التدفق المفتاح العام + PreSharedKey(optional) المعرّف القصير SpiderX المفتاح السري محجوز (اختياري) العنوان المحلي (اختياري IPv4/IPv6، مفصولة بفواصل) - Mtu (اختياري، الافتراضي 1420) + Mtu (اختياري، الافتراضي 1280) نجاح فشل لا يوجد شيء @@ -97,6 +102,7 @@ المحتوى لا توجد بيانات في الحافظة رابط URL غير صالح + Please do not use the insecure HTTP protocol subscription address تأكد من أن منفذ الاتصالات الواردة يتوافق مع الإعدادات تكوين مشوه مضيف (SNI) (اختياري) @@ -104,12 +110,21 @@ إضافة أصل إضافة ملفات إضافة URL + مسح رمز الاستجابة السريعة (QRcode) URL تنزيل الملفات إضافة عنوان URL للأصل الملف غير موجود الملاحظات موجودة بالفعل الإجراء غير مسموح به + Obfs password + Port Hopping + Port Hopping Interval + pinSHA256 + Bandwidth down (units) + Bandwidth up (units) + XHTTP Mode + XHTTP Extra raw JSON, format: { XHTTPObject } جار التحميل @@ -129,6 +144,8 @@ إعدادات VPN الوكيل لكل تطبيق عام: التطبيق المحدد هو وكيل، غير المحدد اتصال مباشر؛ \nوضع التجاوز: التطبيق المحدد متصل مباشرة، غير المحدد وكيل. \nخيار تحديد تطبيق الوكيل تلقائيًا في القائمة + Auto connect at startup + Automatically connects to the selected server at startup, which may be unsuccessful إعدادات Mux تمكين Mux @@ -158,19 +175,18 @@ تفضيل IPv6 تفضيل عنوان IPv6 وطرق التوجيه - التوجيه - استراتيجية النطاق - قواعد محددة مسبقًا - قواعد مخصصة - DNS البعيد (udp/tcp/https/quic) (اختياري) DNS VPN DNS (IPv4/v6 فقط) + Does VPN bypass LAN DNS المحلي (اختياري) DNS + DNS hosts (Format: domain:address,…) + domain:address,… + True delay test url (http/https) Url @@ -178,11 +194,11 @@ يمكن للأجهزة الأخرى الاتصال بالبروكسي بواسطة عنوان IP الخاص بك من خلال socks/http، يتم التمكين فقط في الشبكة الموثوقة لتجنب الاتصال غير المصرح به السماح بالاتصالات من الشبكة المحلية، تأكد من أنك في شبكة موثوقة - منفذ بروكسي SOCKS5 - منفذ بروكسي SOCKS5 + السماح غير الآمن + عند TLS، الافتراضي هو السماح غير الآمن - منفذ بروكسي HTTP - منفذ بروكسي HTTP + منفذ بروكسي Local + منفذ بروكسي Local منفذ DNS المحلي منفذ DNS المحلي @@ -193,6 +209,9 @@ بدء المسح الضوئي على الفور افتح الكاميرا لمسح الرمز ضوئيًا على الفور عند بدء التشغيل، وإلا يمكنك اختيار مسح الرمز ضوئيًا أو تحديد صورة في شريط الأدوات + Append HTTP Proxy to VPN + HTTP proxy will be used directly from (browser/ some supported apps), without going through the virtual NIC device (Android 10+) + ملاحظات ملاحظات التحسينات أو الأخطاء إلى GitHub الانضمام إلى مجموعة Telegram @@ -200,6 +219,7 @@ سياسة الخصوصية حول\nترجمة م. ابراهيم قاسم الكود المصدري + Open Source licenses قناة Telegram نسخ التكوين احتياطيًا موقع التخزين: [%s]، سيتم مسح النسخة الاحتياطية بعد إلغاء تثبيت التطبيق أو مسح التخزين @@ -231,8 +251,12 @@ إعداد مجموعة الاشتراك ملاحظات عنوان URL اختياري + Remarks regular filter تفعيل التحديث تفعيل التحديث التلقائي + Previous proxy remarks + Next proxy remarks + The remarks exists and is unique تحديث الاشتراك (أول خطوة) Tcping لجميع الإعدادات اختبر جميع الإعدادات (3) @@ -242,20 +266,31 @@ جميع مجموعات الاشتراك حذف %d من الإعدادات المكررة + Delete %d configurations + Import %d configurations + Export %d configurations + Update %d configurations بدء الخدمة تأكيد + استراتيجية النطاق إعدادات التوجيه مفصولة بفواصل (،)، تذكر الحفظ حفظ مسح - فحص واستبدال - فحص وإضافة - تعيين قواعد توجيه افتراضية + Routing Rule Settings + Add rule + استيراد مجموعات قواعد محددة مسبقاً + Existing rulesets will be deleted, are you sure to continue? + Import ruleset from clipboard + استيراد مجموعة قواعد من رمز الاستجابة السريعة + Export ruleset to clipboard + Locked, keep this rule when import presets التحقق من الاتصال يجري الاختبار… - نجاح: استغرق اتصال HTTP %dms + Testing %d configurations… + نجاح: ping [ %d , %d ] فشل اكتشاف اتصال الإنترنت: %s الإنترنت غير متاح رمز الخطأ: #%d @@ -281,20 +316,6 @@ تصدير إلى الحافظة - - عنوان URL أو IP الوكيل - عنوان URL أو IP المباشر - عنوان URL أو IP المحظور - - - - وكيل عام - تجاوز عنوان الشبكة المحلية ثم الوكيل - تجاوز عنوان البر الرئيسي ثم الوكيل - تجاوز عنوان الشبكة المحلية والبر الرئيسي ثم الوكيل - مباشر عام - - VPN بروكسي فقط @@ -306,4 +327,10 @@ داكن + + Follow config + Bypass + Not Bypass + + diff --git a/V2rayNG/app/src/main/res/values-bn/strings.xml b/V2rayNG/app/src/main/res/values-bn/strings.xml index ea85d329b6..38670354f6 100644 --- a/V2rayNG/app/src/main/res/values-bn/strings.xml +++ b/V2rayNG/app/src/main/res/values-bn/strings.xml @@ -8,10 +8,12 @@ নেভিগেশন ড্রয়ার বন্ধ করুন ডেটা স্থানান্তর সফল! ডেটা স্থানান্তর ব্যর্থ! + Please pull down to refresh! বন্ধ করুন অনুমতি পাওয়া যাচ্ছে না + Unable to obtain the notification permission আরও দেখতে ক্লিক করুন সার্ভিস শুরু করুন সার্ভিস বন্ধ করুন @@ -25,12 +27,14 @@ কনফিগারেশন মুছুন QR কোড থেকে কনফিগারেশন আমদানি করুন ক্লিপবোর্ড থেকে কনফিগারেশন আমদানি করুন - ম্যানুয়ালি টাইপ করুন [Vmess] + ম্যানুয়ালি টাইপ করুন [VMess] ম্যানুয়ালি টাইপ করুন [VLESS] ম্যানুয়ালি টাইপ করুন [Shadowsocks] - ম্যানুয়ালি টাইপ করুন [Socks] + ম্যানুয়ালি টাইপ করুন [SOCKS] + Type manually[HTTP] ম্যানুয়ালি টাইপ করুন [Trojan] ম্যানুয়ালি টাইপ করুন [Wireguard] + Type manually[Hysteria2] কাস্টম কনফিগারেশন ক্লিপবোর্ড থেকে কাস্টম কনফিগারেশন আমদানি করুন স্থানীয়ভাবে কাস্টম কনফিগারেশন আমদানি করুন @@ -52,14 +56,14 @@ http হোস্ট ws হোস্ট httpupgrade হোস্ট - splithttp হোস্ট + xhttp হোস্ট h2 হোস্ট QUIC নিরাপত্তা gRPC কর্তৃপক্ষ পথ ws পথ httpupgrade পথ - splithttp পথ + xhttp পথ h2 পথ QUIC কী kcp বীজ @@ -78,12 +82,13 @@ এনক্রিপশন ফ্লো পাবলিক কী + PreSharedKey(optional) শর্ট আইডি SpiderX সিক্রেট কী সংরক্ষিত (ঐচ্ছিক) স্থানীয় ঠিকানা (ঐচ্ছিক IPv4/IPv6, কমা দ্বারা পৃথক করা) - MTU (ঐচ্ছিক, ডিফল্ট 1420) + MTU (ঐচ্ছিক, ডিফল্ট 1280) সফল ব্যর্থ কোনও তথ্য নেই @@ -96,6 +101,7 @@ কনটেন্ট ক্লিপবোর্ডে কোনও তথ্য নেই অবৈধ URL + Please do not use the insecure HTTP protocol subscription address ইনবাউন্ড পোর্ট নিশ্চিত করুন সেটিংসের সাথে সামঞ্জস্যপূর্ণ কনফিগারেশন বিকৃত হোস্ট (SNI) (ঐচ্ছিক) @@ -103,12 +109,22 @@ অ্যাসেট যোগ করুন ফাইল যোগ করুন URL যোগ করুন + QR কোড স্ক্যান করুন URL ফাইল ডাউনলোড করুন অ্যাসেট URL যোগ করুন ফাইল খুঁজে পাওয়া যায়নি মন্তব্য ইতিমধ্যে বিদ্যমান অ্যাকশন অনুমোদিত নয় + Obfs password + Port Hopping + Port Hopping Interval + pinSHA256 + Bandwidth down (units) + Bandwidth up (units) + XHTTP Mode + XHTTP Extra raw JSON, format: { XHTTPObject } + লোড হচ্ছে অনুসন্ধান করুন @@ -126,6 +142,8 @@ VPN সেটিংস প্রতি-অ্যাপ প্রক্সি সাধারণ: চেকড অ্যাপ প্রক্সি, আনচেকড সরাসরি সংযোগ; \nবাইপাস মোড: চেকড অ্যাপ সরাসরি সংযুক্ত, আনচেকড প্রক্সি। \nমেনুতে প্রক্সি অ্যাপ্লিকেশন স্বয়ংক্রিয়ভাবে নির্বাচন করার বিকল্প + Auto connect at startup + Automatically connects to the selected server at startup, which may be unsuccessful Mux সেটিংস Mux সক্রিয় করুন @@ -157,19 +175,18 @@ IPv6 অগ্রাধিকার দিন IPv6 ঠিকানা এবং রুটকে অগ্রাধিকার দিন - রাউটিং - ডোমেইন কৌশল - পূর্বনির্ধারিত নিয়ম - কাস্টম নিয়ম - রিমোট DNS (udp/tcp/https/quic)(ঐচ্ছিক) DNS VPN DNS (শুধুমাত্র IPv4/v6) + Does VPN bypass LAN ঘরোয়া DNS (ঐচ্ছিক) DNS + DNS hosts (Format: domain:address,…) + domain:address,… + সঠিক বিলম্ব পরীক্ষা ইউআরএল (http/https) ইউআরএল @@ -180,11 +197,8 @@ allowInsecure যখন TLS, ডিফল্টভাবে allowInsecure - SOCKS5 প্রক্সি পোর্ট - SOCKS5 প্রক্সি পোর্ট - - HTTP প্রক্সি পোর্ট - HTTP প্রক্সি পোর্ট + Local প্রক্সি পোর্ট + Local প্রক্সি পোর্ট স্থানীয় DNS পোর্ট স্থানীয় DNS পোর্ট @@ -194,6 +208,10 @@ তাত্ক্ষণিক স্ক্যান শুরু করুন শুরুতে তাত্ক্ষণিকভাবে স্ক্যান করতে ক্যামেরা খুলুন, অন্যথায় আপনি কোড স্ক্যান বা টুলবারে একটি ছবি নির্বাচন করতে পারেন + + Append HTTP Proxy to VPN + HTTP proxy will be used directly from (browser/ some supported apps), without going through the virtual NIC device (Android 10+) + মতামত মতামত উন্নয়ন বা বাগগুলি GitHub-এ পাঠান টেলিগ্রাম গ্রুপে যোগদান করুন @@ -201,6 +219,7 @@ গোপনীয়তা নীতি সম্পর্কিত সোর্স কোড + Open Source licenses টেলিগ্রাম চ্যানেল কনফিগারেশন ব্যাকআপ স্টোরেজ অবস্থান: [%s], অ্যাপ আনইনস্টল বা স্টোরেজ ক্লিয়ার করার পরে ব্যাকআপ মুছে যাবে @@ -232,8 +251,12 @@ সাবস্ক্রিপশন গ্রুপ সেটিং মন্তব্য ঐচ্ছিক URL + Remarks regular filter আপডেট সক্রিয় করুন স্বয়ংক্রিয় আপডেট সক্রিয় করুন + Previous proxy remarks + Next proxy remarks + The remarks exists and is unique সাবস্ক্রিপশন আপডেট সব কনফিগারেশন TCPing সব কনফিগারেশন প্রকৃত বিলম্ব @@ -242,20 +265,31 @@ কনফিগারেশন ফাইল ফিল্টার করুন সব সাবস্ক্রিপশন গ্রুপ %d ডুপ্লিকেট কনফিগারেশন মুছে ফেলুন + Delete %d configurations + Import %d configurations + Export %d configurations + Update %d configurations সার্ভিস শুরু করুন নিশ্চিত করুন + ডোমেইন কৌশল রাউটিং সেটিংস কমা (,) দ্বারা আলাদা করুন, মনে রাখবেন সেভ করতে সেভ করুন মুছে ফেলুন - স্ক্যান করুন এবং প্রতিস্থাপন করুন - স্ক্যান করুন এবং যোগ করুন - ডিফল্ট রাউটিং নিয়ম সেট করুন + Routing Rule Settings + Add rule + পূর্বনির্ধারিত নিয়মাবলী আমদানি করুন + Existing rulesets will be deleted, are you sure to continue? + Import ruleset from clipboard + QRcode থেকে রুলসেট আমদানি করুন + Export ruleset to clipboard + Locked, keep this rule when import presets সংযোগ পরীক্ষা করুন পরীক্ষা চলছে… - সফল: HTTP সংযোগ নিয়েছে %dms + Testing %d configurations… + সফল: ping [ %d , %d ] ms ইন্টারনেট সংযোগ সনাক্ত করতে ব্যর্থ: %s ইন্টারনেট উপলব্ধ নয় ত্রুটি কোড: #%d @@ -280,20 +314,6 @@ ক্লিপবোর্ডে রপ্তানি করুন - - প্রক্সি URL বা IP - ডাইরেক্ট URL বা IP - ব্লকড URL বা IP - - - - গ্লোবাল প্রোক্সি - LAN ঠিকানা বাইপাস করে তারপর প্রোক্সি - মেইনল্যান্ড ঠিকানা বাইপাস করে তারপর প্রোক্সি - LAN এবং মেইনল্যান্ড ঠিকানা বাইপাস করে তারপর প্রোক্সি - গ্লোবাল ডাইরেক্ট - - VPN শুধুমাত্র প্রোক্সি @@ -304,4 +324,19 @@ লাইট ডার্ক + + চায়না হোয়াইটলিস্ট + চায়না ব্ল্যাকলিস্ট + গ্লোবাল + Iran lite + Iran medium + Iran heavy + + + + Follow config + Bypass + Not Bypass + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml b/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml new file mode 100644 index 0000000000..02f90db216 --- /dev/null +++ b/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml @@ -0,0 +1,353 @@ + + + v2rayNG + آلشت + آلشت + سی کرت ٱولی و کار بوردن ای ویژیی، ز برنومه سی ازاف کردن سرور استفاڌه کۊنین + گۊشیڌن نومگه کشاری + بستن نومگه کشاری + مووفقیت من جاگورویی داده + جاگورویی داده ٱنجوم نگرؽڌ + Please pull down to refresh! + + + واڌاشتن + گرؽڌن موجوز مومکن نؽڌ + گرؽڌن موجوز وارسۊوی مومکن نؽڌ + سی گرؽڌن دۉسمندیا بیشتر کیلیک کوݩ + ر وستن خدمات + واڌاشتن خدمات + ر وستن خدمات وا مووفقیت ٱنجوم وابی + ر وستن خدمات وا مووفقیت ٱنجوم نوابی + + + فایل کانفیگ + ٱووردن کانفیگ + زفت کردن کانفیگ + پاک کردن کانفیگ + و من ٱووردن کانفیگ ز QRcode + و من ٱووردن کانفیگ ز کلیپ بورد + هؽل دستی[VMess] + هؽل دستی[VLESS] + هؽل دستی[Shadowsocks] + هؽل دستی[SOCKS] + هؽل دستی[HTTP] + هؽل دستی[Trojan] + هؽل دستی[Wireguard] + هؽل دستی[Hysteria2] + کانفیگ سفارشی + کانفیگ سفارشین ز کلیپ بورد و من بیار + کانفیگ سفارشین ز مهلی و من بیار + کانفیگ سفارشین ز آدرس اینترنتی و من بیار + نشۊوی اینترنتی اسکن کانفیگ سفارشین بزݩ + پاک بۊ؟ + پؽش ز پاک کردن کانفیگ نا موئتبر واجۊری کوݩ! پاک کردن کانفیگن قوۊل اکۊنی؟ + نیشتنا + آدرس + پورت + نوم من توری + شناسه جایگۊزین + ٱمنیت + شبکه + جاگورو + نوء سر بلگ + هالت gRPC + هاست + هاست http + هاست ws + هاست httpupgrade + هاست xhttp + هاست h2 + ٱمنیت QUIC + Authority gRPC + تور + تور WS + تور HTTPUpgrade + تور XHTTP + تور H2 + تور QUIC + KCP seed + نوم خدمات gRPC + TLS + Fingerprint + Alpn + اجازه نا ٱمن + SNI + آدرس + پورت + رزم + ٱمنیت + رزم (اختیاری) + نوم من توری (اختیاری) + رزم نگاری + جریان + کیلیت پوی وولاتی + کیلیت رمز ناهاڌن ازاف (اختیاری) + ShortID + SpiderX + کیلیت سیخومی + Reserved(اختیاری، وا کاما ز یک جوڌا ابۊن) + آدرس مهلی (اختیاری IPv4/IPv6، وا کاما ز یک جوڌا ابۊن) + Mtu(اختیاری، پؽش فرز 1280) + وا مووفقیت ٱنجوم وابی + شکست خرد + هیچ داده ای وۊجۊڌ نڌاره + پوروتوکول نادوروست + رزم گوشایی ٱنجوم نوابی + پسند فایل کانفیگ + ی برنومه دؽوۉداری فایل بپۊرنین. + کانفیگ سفارشی + کانفیگ نا موئتبر هڌ + موئتوا + هیچ داده ای من کلیپ بورد وۊجۊڌ نڌاره + نشۊوی اینترنتی نا موئتبر هڌ + آدرس اشتراک پوروتوکول نا ٱمن HTTP ن و کار مبرین + موتمعن بۊین ک پورت وۊرۊڌی وا سامووا ی جۊر هڌ + کانفیگ زبال نؽڌ + هاست(SNI)(اختیاری) + لف گیری فایل ٱنجوم نوابی، ز ی برنومه دؽوۉداری فایل هیاری بگرین + ازاف کردن دارایی + ازاف کردن فایل + ازاف کردن لینگ + اسکن QRcode + آدرس اینترنتی + دانلود فایلا + آدرس اینترنتی دارایین ازاف کۊنین + فایلن نجوست + ائزارات ز زیتر بیڌسۉݩ + ای کار ممنۊ هڌ + رزم obfs + پورت گوم (درگا سرورن ز نۊ هؽل اکونه) + فاسله پورت گوم (سانیه) + pinSHA256 + ب لم ٱووڌن پئنا باند (واهڌ) + وا روء رئڌن پئنا باند (واهڌ) + هالت XHTTP + XHTTP Extra خام JSON، قالوو: { XHTTPObject } + + + هون بار ونی بۊ + پیتینیڌن + پسند پوی + رزمان بزنین + هالت Bypass + پسند خوتکار پروکسی برنومه + موئتوا هونی دانلود ابۊن + و در کشیڌن من کلیپ بورد + و من ٱووردن ز کلیپ بورد + + + + سامووا + سامووا پؽش رئڌه + سامووا VPN + پروکسی و ری برنومه + پوی وولاتی: برنومه واجۊری بیڌه پروکسی هڌ، منپیز موستقیم بؽ نشووه هڌ. هالت دور زیڌن: برنومه نشووک ناڌه موستقیمن منپیز هڌ، پروکسی نشووک زیڌه نؽڌ. گۊزینه پسند خوتکار برنومه پروکسی من نومگه + منپیز خوتکار مجال ر ونی + مجال ر وندن، خوساخوس و سرور پسند بیڌه منپیز ابۊ که گاشڌ نا مووفق بۊ. + + سامووا Mux + ر وندن Mux + زل تر، ٱما گاشڌ منپیز زی قت بۊ بارت دؽوۉداری، TCP، UDP و QUIC ن ای لم سفارشی کۊنین. + منپیزا TCP (تلایه منجا 1-1024) + منپیزا XUDP (تلایه منجا 1-1024) + دؽوۉداری QUIC من تونل mux + + رڌ کردن + موجاز + گوم زیڌن + + + ر وندن نشۉݩ داڌن سورعت + نشۉݩ داڌن سورعت هیم سکویی من وارسۊویا. نماڌ وارسۊوی و ری و کار گرؽڌن آلشت ابۊ. + + ر وندن Sniffing + دامنه sniff ن ز کتن امتهۉݩ کۊنین (پؽش فرز رۊشن) + ر وندن routeOnly + ز نوم دامنه sniffed تینا سی تور جوستن استفاڌه کۊنین وو آدرس مورد نزرن و عونوان آدرس IP ووردارین. + + ر وندن DNS مهلی + DNS پردازشت وابیڌه و دس هسته ماژول DNS (پؽشنهاڌ ابۊ، ٱر نیاز هڌ ک جوستن تور وو ولات ٱسلین دور زنی) + + ر وندن DNS جئلی + DNS مهلی آدرسا IP جئلی ن وورگنه (زل تر، ٱما گاشڌ من یقرد ز برنومیل کار نکونه) + + ترجی IPv6 + ترجی داڌن نشۊوی وو تورا IPv6 + + DNS ز ر دیر (اختیاری) (udp/tcp/https/quic) (اختیاری) + DNS + + VPN DNS (تینا IPv4/v6) + VPN ز شبکه مهلی اگوڌرته؟ + + DNS منی (اختیاری) + DNS + + DNS هاست موستقیم (قالوو: دامنه: آدرس،...) + دامنه:آدرس،... + + آدرس اینترنتی آزمایش تئخیر واقعی (http/https) + نشۊوی اینترنتی + + هشتن منپیزا ز شبکه مهلی + پوی دسگایل ترن وا آدرس IP ایسا، ز ر socks/http و پروکسی منپیز بۊن، تینا من شبکه قابل اعتماد فعال بۊ تا ز منپیز غیر موجاز جلو گری بۊ. + منپیزا ز شبکه مهلی ن موجار کۊنین، موتمعن بۊین ک من ی شبکه قابل ائتماڌ هڌین. + + اجازه نا ٱمن + مجال و کار بوردن TLS ب تۉر پؽش فرز، موجوز نا ٱمن فعال هڌ. + + پورت پروکسی مهلی + پورت پروکسی مهلی + + پورت DNS مهلی + پورت DNS مهلی + + قوۊل کردن پاک کردن کانفیگ + سی پاک وابیڌن فایل کانفیگ نیاز به قوۊل کردن دووارته ز سمت منتور هڌ + + زی اسکنن ر ون + شؽواتگرن سی اسکن، زی مجال ر وندن بۊگۊشین، اندی ترین کودن اسکن کۊنین یا شؽواتی ن منه نوار ٱوزار پسند کۊنین. + + پروکسی HTTP ن و VPN ازاف کۊنین + پروکسی HTTP ن موسقیمن ز (مۊرۊرگر/ی قرد ز برنومیل لادراری بیڌه)، بؽ استفاڌه ز دسگا NIC مجازی (Android 10+) استفاڌه ابۊ. + + فشناڌن منشڌ + فشناڌن منشڌ یا داسوو موشکلا من Github + ٱووڌن من جرگه تلگرام + برنومه تلگرامن نجوست + هریم سیخومی + زبار + کود بونچک + موجوزا کود بونچک + تورگه تلگرام + لادراری گرؽڌن ز کانفیگ + جاگه زفت کردن: [%s]، بعڌ پاک کردن برنومه یا پاک کردن جاگه زفت کردن، نوسخه لادرار هم پاک ابۊ. + وورگندن کانفیگ + یک رسۊوی کانفیگ + + تبلیقات + تبلیقات، سی نیشتن جوزیات بیشتر کیلیک کۊنین. (هیاری مالی کۊنین تا پاک بۊ) + + ورۊ کردن خوتکار اشتراکا + اشتراکا خوتۉ ن و تۉر خوتکار وا فاسله زمۊوی من پس زمینه ورۊ کۊنین. ای ویژیی من پوی دسگایل گاشڌ همیشه کار نکونه + فاسله ورۊ کردن خوتکار (اقلن وا 15 دؽقه بۊ) + + سئت داسووا + هالت + سی دووسمندیا وو هیاری بیشتر، ری ای هؽل بزݩ + زۉݩ + سامووا رابت منتوری + سامووا هالت رابت منتوری + + داسووا + لف گیری + روفتن + ر وندن دووارته خدمات + پاک کردن پوی کانفیگا جرگه سکویی + پاک کردن کانفیگا تکراری جرگه سکویی + پاک کردن کانفیگا نا موئتبر جرگه سکویی + و در کشیڌن کانفیگا غیر سفارشی جرگه سکویی من کلیپ بورد + سامووا جرگه اشتراک + نیشتنا + نشۊوی اینترنتی اختیاری + نوم موستعار فیلتر + فعال بیڌن ورۊ کردن + فعال بیڌن ورۊ کردن خوتکار + نوم موستعار پروکسی دیندایی + نوم موستعار پروکسی نیایی + موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی + ورۊ کردن اشتراک جرگه سکویی + Tcping کانفیگا جرگه سکویی + تئخیر واقعی کانفیگا جرگه سکویی + فایلا دارایی جوقرافیایی + ترتیب و ری نتیجیل آزمایش + فیلتر کردن کانفیگا + پوی جرگیل + پاک کردن %d کانفیگ تکراری + + پاک کردن %d کانفیگ + و من ٱووردن %d کانفیگ + و در کشیڌن %d کانفیگ + ورۊ کردن %d کانفیگ + ر وندن خدمات + قوۊل + + نشقه دامنه + سامووا تور جوستن + وا کاما ز یک جوڌا ابۊن (،) پسند دامنه یا آی پی + زفت کردن + روفتن + سامووا قانۉݩ تور جوستن + ازاف کردن قانۉݩ + و من ٱووردن قانووا + قانووایی ک هیم سکو هڌسۉݩ پاک ابۊن، هنی هم اخۊی پاکسۉݩ کۊنی؟ + و من ٱووردن قانووا ز کلیپ بورد + و من ٱووردن قانووا ز QRcode + و در کشیڌن قانووا وو زفت من کلیپ بورد + چفت هڌ، ای قانؤنن مجال و من ٱووردن ز پؽش سامووا زفت کۊنین + domain + ip + port + protocol + [http,tls,bittorrent] + network"[udp|tcp]" + [udp|tcp] + outboundTag + + منپیزن واجۊری کوݩ + هونی آزمایش ابۊ… + %d کانفیگ هونی آزمایش ابۊ... + موفق: پینگ [ %d , %d ] + منپیز و اینترنتن نجوست: %s + اینترنت من دسرس نؽ + کود ختا: #%d + منپیز هڌ، سی واجۊری کیلیک کوݩ + منپیز نؽڌ + + اشتراک وا مووفقیت زفت زابی + اشتراک زفت نوابی + سامووا Fragment + Fragment Packets + Fragment Length (min-max) + Fragment Interval (min-max) + ر وندن Fragment + + + QRcode + و در کشیڌن من کلیپ بورد + و در کشیڌن پوی کانفیگ من کلیپ بورد + + + + QRcode + و در کشیڌن من کلیپ بورد + + + + VPN + تینا پروکسی + + + + و دین کردن سیستوم + رۊشنا + تاریک + + + + نومگه اسبؽڌ چین + نومگه شه چین + جهۊوی (Global) + Iran lite + Iran medium + Iran heavy + + + + پؽش فرز کانفیگ + دور زیڌه بۊ + دور زیڌه نبۊ + + + diff --git a/V2rayNG/app/src/main/res/values-fa/strings.xml b/V2rayNG/app/src/main/res/values-fa/strings.xml index 0aebdeda28..edf6443f94 100644 --- a/V2rayNG/app/src/main/res/values-fa/strings.xml +++ b/V2rayNG/app/src/main/res/values-fa/strings.xml @@ -1,5 +1,6 @@ + v2rayNG تعویض تعویض برای اولین بار از این ویژگی استفاده می‌کنید، لطفا از برنامه برای افزودن سرور استفاده کنید @@ -7,15 +8,17 @@ بستن منو کشویی موفقیت در انتقال داده انتقال داده انجام نشد! + لطفاً برای تازه کردن، پایین بکشید! توقف - قادر به دریافت مجوز نیست - برای اطلاعات بیشتر کلیک کنید + دریافت مجوز امکان پذیر نیست + دریافت مجوز اعلان امکان پذیر نیست + برای کسب اطلاعات بیشتر کلیک کنید شروع خدمات توقف خدمات - خدمات با موفقیت شروع شد - شروع خدمات انجام نشد! + شروع خدمات با موفقیت انجام شد + شروع خدمات با موفقیت انجام نشد! فایل کانفیگ @@ -23,69 +26,72 @@ ذخیره کانفیگ حذف کانفیگ کانفیگ را از QRcode وارد کنید - کانفیگ را از کلیپ‌بورد وارد کنید - تایپ دستی[Vmess] + کانفیگ را از کلیپ ‌بورد وارد کنید + تایپ دستی[VMESS] تایپ دستی[VLESS] - تایپ دستی[Shadowsocks] - تایپ دستی[Socks] - تایپ دستی[Trojan] - [Wireguard]تایپ دستی + تایپ دستی[SHADOWSOCKS] + تایپ دستی[SOCKS] + تایپ دستی[HTTP] + تایپ دستی[TROJAN] + ‌تایپ دستی[WIREGUARD] + تایپ دستی[HYSTERIA2] کانفیگ سفارشی - کانفیگ سفارشی را از کلیپ‌بورد وارد کنید + کانفیگ سفارشی را از کلیپ ‌بورد وارد کنید کانفیگ سفارشی را به صورت محلی وارد کنید کانفیگ سفارشی را از طریق نشانی اینترنتی وارد کنید نشانی اینترنتی اسکن کانفیگ سفارشی را وارد کنید حذف شود؟ - Please test before deleting! Confirm delete ? + لطفا قبل از حذف کانفیگ نامعتبر بررسی کنید! حذف کانفیگ را تایید می کنید؟ ملاحظات نشانی پورت شناسه - alterId + شناسه جایگزین امنیت شبکه انتقال - نوع head + نوع سربرگ حالت gRPC - host - http host - ws host - httpupgrade host - splithttp host - h2 host + هاست + هاست HTTP + هاست WS + هاست HTTPUpgrade + هاست XHTTP + هاست H2 QUIC security gRPC Authority - path - ws path - httpupgrade path - splithttp path - h2 path - QUIC key - kcp seed - gRPC serviceName + مسیر + مسیر WS + مسیر HTTPUpgrade + مسیر XHTTP + مسیر H2 + مسیر QUIC + KCP seed + gRPC ServiceName TLS اثرانگشت Alpn - مجوز ناامن + اعطای مجوز ناامن SNI نشانی پورت رمز عبور امنیت رمز عبور (اختیاری) - نام‌کاربری (اختیاری) + نام‌ کاربری (اختیاری) رمزنگاری جریان - PublicKey - ShortId + کلید عمومی + کلید رمزگذاری اضافی (اختیاری) + ShortID SpiderX - SecretKey - Reserved (اختیاری) - آدرس محلی IPv4(اختیاری) - Mtu(optional, default 1420) + کلید خصوصی + Reserved (اختیاری، جدا شده با کاما) + آدرس محلی IPV4 (اختیاری) + MTU (اختیاری، پیش‌فرض 1280) با موفقیت انجام شد شکست - چیزی نیست + هیچ داده ای وجود ندارد پروتکل نادرست رمزگشایی انجام نشد انتخاب فایل کانفیگ @@ -93,44 +99,56 @@ کانفیگ سفارشی کانفیگ معتبر نیست محتوا - هیچ داده‌ای در کلیپ‌بورد وجود ندارد + هیچ داده‌ ای در کلیپ ‌بورد وجود ندارد نشانی اینترنتی معتبر نیست + لطفاً از آدرس اشتراک پروتکل HTTP ناامن استفاده نکنید اطمینان حاصل کنید که پورت ورودی با تنظیمات مطابقت دارد کانفیگ درست نیست - میزبان (SNI) (اختیاری) + هاست (SNI) (اختیاری) کپی فایل انجام نشد، لطفا از برنامه مدیریت فایل استفاده کنید - افزودن فایل‌ها + افزودن فایل ‌ها + اسکن QRcode URL - دانلود فایل‌ها + دانلود فایل‌ ها این عمل ممنوع است + رمز عبور obfs + پورت پرش (درگاه سرور را بازنویسی می کند) + فاصله پورت پرش (ثانیه) + pinSHA256 + کاهش پهنای باند (واحد) + افزایش پهنای باند (واحد) + حالت XHTTP + خام JSON XHTTP Extra، قالب: { XHTTPObject } - URL را اضافه کنید + آدرس اینترنتی را اضافه کنید فایل پیدا نشد نام قبلاً وجود دارد بارگذاری جستجو انتخاب همه - کلیدواژه‌ها را وارد کنید - حالت Bypass + کلیدواژه‌ ها را وارد کنید + حالت دور زدن انتخاب خودکار پروکسی برنامه در حال دانلود محتوا - خروجی گرفتن در کلیپ‌بورد - وارد کردن از کلیپ‌بورد + خروجی گرفتن در کلیپ‌ بورد + وارد کردن از کلیپ‌ بورد تنظیمات تنظیمات پیشرفته تنظیمات VPN پروکسی به تفکیک برنامه - عمومی: برنامه بررسی شده پروکسی است، اتصال مستقیم بدون بررسی است. \nحالت bypass: برنامه بررسی شده مستقیما متصل است، پراکسی بررسی نشده است. \nگزینه‌ای برای انتخاب خودکار پروکسی برنامه در منو است + عمومی: برنامه بررسی شده پروکسی است، اتصال مستقیم بدون بررسی است. \nحالت دور زدن: برنامه بررسی شده مستقیما متصل است، پراکسی بررسی نشده است. \nگزینه‌ای برای انتخاب خودکار پروکسی برنامه در منو است. + اتصال خودکار هنگام راه اندازی + هنگام راه اندازی به طور خودکار به سرور انتخابی متصل می شود که ممکن است ناموفق باشد. - تنظیمات Mux - فعال کردن Mux - سریعتر است، اما ممکن است باعث اتصال ناپایدار شود\nمخزن ترافیک TCP با 8 اتصال پیش‌فرض، نحوه مدیریت UDP و QUIC را در زیر سفارشی کنید + تنظیمات MUX + فعال کردن MUX + سریعتر است، اما ممکن است باعث اتصال ناپایدار شود\nمخزن ترافیک TCP با 8 اتصال پیش‌فرض، نحوه مدیریت UDP و QUIC را در زیر سفارشی کنید. اتصالات TCP (محدوده -1 تا 1024) اتصالات XUDP (محدوده -1 تا 1024) - مدیریت QUIC در تونل mux + مدیریت QUIC در تونل MUX رد کردن مجاز @@ -138,49 +156,47 @@ فعال کردن نمایش سرعت - نمایش سرعت فعلی در قسمت آگاه‌سازی. \nآیکون آگاه‌سازی بر اساس استفاده تغییر می‌کند. + نمایش سرعت فعلی در قسمت اعلان. \nآیکون اعلان بر اساس استفاده تغییر می‌کند. - فعال کردن Sniffing - دامنه sniff را از بسته امتحان کنید (پیش‌فرض روشن) - Enable routeOnly - Use the sniffed domain name for routing only, and keep the target address as the IP address. + فعال کردن تجزیه و تحلیل بسته ها (Sniffing) + استفاده از تشخیص نام دامنه (Sniff) در بسته ها (به طور پیش فرض فعال است) + فعال کردن دامنه فقط مسیر یابی (RouteOnly) + از نام دامنه (Snnifed) فقط برای مسیریابی استفاده کنید و آدرس مقصد را به عنوان IP ذخیره کنید. فعال کردن DNS محلی - DNS پردازش شده توسط ماژول DNS هسته (توصیه می‌شود، در صورت نیاز به دور زدن LAN و نشانی mainland) + درخواست های DNS به هسته وارد شده و توسط ماژول DNS پردازش می شوند (توصیه می شود در صورت نیاز به مسیریابی برای دور زدن آدرس های LAN و سرزمین اصلی فعال شود) فعال کردن DNS جعلی - DNS محلی آدرس IP جعلی را برمی‌گرداند (سریع‌تر می‌باشد، اما ممکن است برای برخی از برنامه‌ها کار نکند) + دی ان اس محلی آدرس های آیپی جعلی را بر می گرداند (سریع تر می باشد و تاخیر را کاهش می دهد اما ممکن است برای برخی از برنامه ها کار نکند) - ترجیح دادن IPv6 + ترجیح دادن IPV6 ترجیح دادن نشانی و مسیر های IPv6 - مسیریابی - استراتژی دامنه - قوانین از پیش تعریف شده - قوانین سفارشی - - DNS از راه دور (اختیاری) + DNS از راه دور (اختیاری) (udp/tcp/https/quic) DNS VPN DNS (فقط IPv4/v6) + آیا VPN از شبکه محلی عبور می کند؟ DNS داخلی (اختیاری) DNS - True delay test url (http/https) - Url + DNS مستقیم هاست (فرمت: دامنه:آدرس،…) + دامنه:آدرس،… - اجازه اتصالات از طریق LAN - دستگاه‌های دیگر می‌توانند از طریق socks/http به پراکسی توسط نشانی آی‌پی شما متصل شوند، فقط در شبکه مورد اعتماد فعال می‌شوند تا از اتصال غیرمجاز جلوگیری کنند - اتصالات از طریق LAN را مجاز کنید، مطمئن شوید که در یک شبکه قابل اعتماد هستید + آدرس اینترنتی آزمایش تاخیر واقعی کانفیگ ها (HTTP/HTTPS) + URL + اجازه اتصالات از طریق شبکه محلی + سایر دستگاه ها می توانند با استفاده از آدرس آیپی شما برای استفاده از یک پروکسی محلی متصل شوند. فقط در یک شبکه قابل اعتماد برای جلوگیری از اتصالات غیرمجاز استفاده کنید. + اتصالات از طریق شبکه محلی را مجاز کنید، مطمئن شوید که در یک شبکه قابل اعتماد هستید. - پورت پروکسی SOCKS5 - پورت پروکسی SOCKS5 + اعطای مجوز ناامن + هنگام استفاده از TLS، به طور پیش‌ فرض مجوز ناامن فعال است. - پورت پروکسی HTTP - پورت پروکسی HTTP + پورت پروکسی محلی + پورت پروکسی محلی پورت DNS محلی پورت DNS محلی @@ -191,68 +207,95 @@ فورا اسکن را شروع کن دوربین را برای اسکن بلافاصله در هنگام راه اندازی باز کنید، در غیر این صورت می توانید کد را اسکن کنید یا عکسی را در نوار ابزار انتخاب کنید. + پروکسی HTTP را به VPN اضافه کنید + پروکسی HTTP مستقیماً از (مرورگر/برخی برنامه‌های پشتیبانی‌شده)، بدون استفاده از دستگاه NIC مجازی (Android 10+) استفاده می‌شود. + بازخورد - بازخورد یا گزارش اشکالات در گیت‌هاب + بازخورد یا گزارش اشکالات در گیت‌ هاب عضویت در گروه تلگرام برنامه تلگرام پیدا نشد حریم خصوصی درباره - Source code - Telegram channel - Backup configuration - Storage location: [%s], The backup will be cleared after uninstalling the app or clearing the storage - Restore configuration - Share configuration + کد منبع + مجوز های منبع باز + کانال تلگرام + پشتیبان گیری از پیکربندی + محل ذخیره سازی: [%s], پس از حذف نصب برنامه یا پاک کردن فضای ذخیره سازی، نسخه پشتیبان پاک می شود + بازیابی پیکربندی + اشتراک گذاری پیکربندی تبلیغات تبلیغات، برای جزئیات بیشتر کلیک کنید (کمک مالی کنید تا حذف شود) - به‌روزرسانی خودکار اشتراک ها - اشتراک های خود را به طور خودکار با فاصله زمانی در پس زمینه به روز کنید. بسته به دستگاه، این ویژگی ممکن است همیشه کار نکند - فاصله به‌روزرسانی خودکار (دقیقه، حداقل مقدار 15) + به‌ روزرسانی خودکار اشتراک ها + اشتراک های خود را به طور خودکار با فاصله زمانی در پس زمینه به روز کنید. بسته به دستگاه، این ویژگی ممکن است همیشه کار نکند. + فاصله به‌ روزرسانی خودکار ( حداقل مقدار ، 15 دقیقه ) سطح گزارشات حالت - برای راهنمایی بیشتر روی این متن، کلیک کنید + برای اطلاعات و راهنمایی بیشتر، روی این متن کلیک کنید زبان - تنظیمات رابط کاربری - UI mode settings + تنظیمات رابط کاربری + تنظیمات حالت رابط کاربری گزارشات کپی پاک کردن راه‌اندازی مجدد خدمات - حذف تمام کانفیگ - حذف کانفیگ های تکراری - حذف کانفیگ‌های نامعتبر (ابتدا آزمایش کنید) - خروجی گرفتن کانفیگ‌های غیرسفارشی در کلیپ‌بورد - تنظیمات گروه‌ی اشتراک + حذف تمام کانفیگ های گروه فعلی + حذف کانفیگ های تکراری گروه فعلی + حذف کانفیگ های نامعتبر گروه فعلی (ابتدا آزمایش کنید) + خروجی گرفتن کانفیگ های غیرسفارشی گروه فعلی در کلیپ ‌بورد + تنظیمات گروه‌ اشتراک ملاحظات نشانی اینترنتی اختیاری + نام مستعار فیلتر فعال کردن به‌روزرسانی فعال سازی به‌روزرسانی خودکار - به‌روزرسانی اشتراک - Tcping همه کانفیگ - تاخیر واقعی همه کانفیگ - فایل‌های دارایی جغرافیا - مرتب‌سازی بر اساس نتایج آزمایش + نام مستعار پروکسی قبلی + نام مستعار پروکسی بعدی + لطفاً مطمئن شوید که نام مستعار وجود دارد و منحصر به فرد است + به‌روزرسانی گروه فعلی اشتراک + TCPING کانفیگ های گروه فعلی + تاخیر واقعی کانفیگ های گروه فعلی + فایل ‌های دارایی جغرافیا + مرتب‌ سازی بر اساس نتایج آزمایش فیلتر کردن کانفیگ‌ها همه گروه‌های اشتراک حذف %d کانفیگ تکراری + حذف %d کانفیگ + وارد کردن %d کانفیگ + صادر کردن %d کانفیگ + آپدیت کردن %d کانفیگ شروع خدمات تایید + استراتژی دامنه تنظیمات مسیریابی - با کاما (,) از هم جدا شوند، ذخیره کردن فراموش نشود + با کاما (،) از هم جدا شوند، ذخیره کردن فراموش نشود ذخیره - پاک کردن - اسکن و جایگزین کنید - اسکن و اضافه کنید - قوانین مسیریابی پیش‌فرض را تنظیم کنید + حذف + تنظیمات قانون مسیریابی + اضافه کردن قانون + بازگردانی قوانین پیش فرض … + مجموعه قوانین موجود حذف خواهند شد، آیا مطمئن هستید که ادامه می دهید؟ + وارد کردن مجموعه قوانین از کلیپ بورد + وارد کردن مجموعه قوانین از QRcode + صادر کردن مجموعه قوانین به کلیپ بورد + قفل است، این قانون را هنگام وارد کردن از پیش تنظیم‌ ها حفظ کنید + دامنه + آیپی + پورت + پورتکل + [http,tls,bittorrent] + شبکه + [udp|tcp] + برچسب خروجی اتصال را بررسی کنید در حال آزمایش... - موفقیت: اتصال HTTP %dms طول کشید + تست کردن %d کانفیگ… + موفق: پینگ [ %d , %d ] اتصال به اینترنت شناسایی نشد: %s اینترنت در دسترس نیست کد خطا: #%d @@ -261,47 +304,48 @@ اشتراک با موفقیت ذخیره شد ذخیره اشتراک ناموفق بود - تنظیمات Fragment - Fragment Packets - Fragment Length (min-max) - Fragment Interval (min-max) - فعال کردن Fragment + تنظیمات فرگمنت + بسته های فرگمنت + طول بسته های فرگمنت (حداقل-حداکثر) + فاصله بین بسته های فرگمنت (حداقل-حداکثر) + فعال کردن فرگمنت QRcode - خروجی گرفتن در کلیپ‌بورد - خروجی گرفتن کانفیگ کامل در کلیپ‌بورد + خروجی گرفتن در کلیپ‌ بورد + خروجی گرفتن کانفیگ کامل در کلیپ بورد QRcode - خروجی گرفتن در کلیپ‌بورد - - - - نشانی اینترنتی یا آی‌پی پروکسی - نشانی اینترنتی یا آی‌پی مستقیم - نشانی اینترنتی یا آی‌پی مسدود شده - - - - پروکسی سراسری - دور زدن آدرس LAN و سپس پروکسی - دور زدن آدرس mainland و سپس پروکسی - دور زدن LAN و آدرس mainland و سپس پروکسی - مستقیم سراسری + خروجی گرفتن در کلیپ‌ بورد VPN فقط پروکسی - افزودن + افزودن منبع افزودن لینک - Follow system - Light - Dark + پیش فرض سیستم + روشن + تاریک + + + + لیست سفید چین + لیست سیاه چین + جهانی(GLOBAL) + ایران سبک + ایران متوسط + ایران سنگین + + + + پیش فرض کانفیگ + دور زده شود + دور زده نشود diff --git a/V2rayNG/app/src/main/res/values-night/styles.xml b/V2rayNG/app/src/main/res/values-night/themes.xml similarity index 100% rename from V2rayNG/app/src/main/res/values-night/styles.xml rename to V2rayNG/app/src/main/res/values-night/themes.xml diff --git a/V2rayNG/app/src/main/res/values-ru/strings.xml b/V2rayNG/app/src/main/res/values-ru/strings.xml index 10bd5cb90f..3659799cb2 100644 --- a/V2rayNG/app/src/main/res/values-ru/strings.xml +++ b/V2rayNG/app/src/main/res/values-ru/strings.xml @@ -7,10 +7,12 @@ Закрыть панель навигации Успешный перенос данных! Перенос данных не выполнен! + Потяните вниз для обновления! Остановить Разрешение не получено + Разрешение на отображение уведомлений не получено Ещё… Запуск служб Остановка служб @@ -24,12 +26,14 @@ Удалить профиль Импорт из QR-кода Импорт из буфера обмена - Ручной ввод Vmess + Ручной ввод VMess Ручной ввод VLESS Ручной ввод Shadowsocks - Ручной ввод Socks + Ручной ввод SOCKS + Ручной ввод HTTP Ручной ввод Trojan - Ручной ввод Wireguard + Ручной ввод WireGuard + Ручной ввод Hysteria2 Другой профиль Импорт из буфера обмена Импорт из файла @@ -37,28 +41,28 @@ Импорт сканированием URL Подтверждаете удаление? Выполните проверку перед удалением! Подтверждаете удаление? - Описание + Название Адрес Порт ID Альтернативный ID Безопасность Сеть - Другие параметры + Транспорт Тип заголовка Режим gRPC Узел Узел HTTP Узел WS Узел HTTPUpgrade - Узел SplitHTTP + Узел XHTTP Узел H2 Шифрование QUIC Полномочия gRPC Путь Путь WS Путь HTTPUpgrade - Путь SplitHTTP + Путь XHTTP Путь H2 Ключ QUIC Сид KCP @@ -76,13 +80,14 @@ Пользователь (необязательно) Шифрование Поток - PublicKey + Открытый ключ + Дополнительный ключ шифрования (необязательно) ShortID SpiderX - SecretKey - Reserved (необязательно) + Закрытый ключ + Reserved (необязательно, через запятую) Локальный адрес (необязательно, IPv4/IPv6 через запятую) - MTU (необязательно, по умолчанию 1420) + MTU (необязательно, по умолчанию 1280) Успешно Ошибка Ничего нет @@ -95,6 +100,7 @@ Данные В буфере обмена нет данных Неправильный URL + Не используйте небезопасный HTTP-протокол в адресе подписки Убедитесь, что входящий порт соответствует настройкам Профиль повреждён Узел (SNI) (необязательно) @@ -102,13 +108,21 @@ Добавить ресурс Добавить файлы Добавить URL + Сканировать QR-код URL Загрузить файлы Добавить URL ресурса Файл не найден - Описание уже существует + Название уже существует Это действие запрещено - + Пароль obfs + Смена портов (переопределяет порт) + Интервал смены портов + pinSHA256 + Входящая пропускная способность (единицы) + Исходящая пропускная способность (единицы) + Режим XHTTP + Необработанный JSON XHTTP Extra, формат: { XHTTPObject } Загрузка… @@ -127,7 +141,9 @@ Расширенные настройки Настройки VPN Прокси для выбранных приложений - Основной: выделенное приложение соединяется через прокси, не выделенное — напрямую;\n\nРежим обхода: выделенное приложение соединяется напрямую, не выделенное — через прокси.\n\nЕсть возможность автоматического выбора проксируемых приложений в меню. + Основной: выделенное приложение соединяется через прокси, не выделенное — напрямую;\nРежим обхода: выделенное приложение соединяется напрямую, не выделенное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню. + Автоподключение при запуске + Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным) Настройки мультиплексирования Использовать мультиплексирование @@ -147,7 +163,7 @@ Анализ пакетов Использовать определение доменных имён в пакетах (по умолчанию включено) Домен только для маршрутизации - Использовать определённое доменное имя только для маршрутизации и сохранять целевой адрес в виде IP. + Использовать доменное имя только для маршрутизации и сохранять целевой адрес в виде IP. Использовать локальную DNS Обслуживание выполняется DNS-модулем ядра (в настройках маршрутизации рекомендуется выбрать режим «Все, кроме LAN и Китая») @@ -158,35 +174,31 @@ Предпочитать IPv6 Предпочитать IPv6-адреса и маршрутизацию - Маршрутизация - Доменная стратегия - Режим маршрутизации - Пользовательские правила - Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно) DNS VPN DNS (только IPv4/v6) + VPN пропускает LAN Внутренняя DNS (необязательно) DNS + Узлы DNS (формат: домен:адрес,…) + домен:адрес,… + Сервис проверки времени отклика (HTTP/HTTPS) URL Разрешать подключения из LAN - Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать прокси по протоколам SOCKS/HTTP. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения. - Доступ из LAN разрешён, убедитесь, что вы находитесь в надёжной сети - + Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать локальный прокси. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения. + Доступ из LAN разрешён, убедитесь, что вы находитесь в доверенной сети - Порт SOCKS5-прокси - Порт SOCKS5-прокси - Порт HTTP-прокси - Порт HTTP-прокси + Порт локального прокси + Порт локального прокси - Локальный порт DNS - Локальный порт DNS + Порт локальной DNS + Порт локальной DNS Подтверждение удаления профиля Требовать двойное подтверждение удаления профиля @@ -194,6 +206,9 @@ Сканирование при запуске Начинать сканирование сразу при запуске приложения или запускать функцию сканирования камерой или из изображения через панель инструментов + Дополнительный HTTP-прокси + HTTP-прокси будет использоваться напрямую (из браузера и других поддерживающих приложений), минуя виртуальный сетевой адаптер (Android 10+) + Обратная связь Предложить улучшение или сообщить об ошибке на GitHub Присоединиться к группе в Telegram @@ -201,6 +216,7 @@ Политика конфиденциальности О приложении Исходный код + Лицензии открытого исходного кода Telegram-канал Резервирование конфигурации Путь: [%s]. Резервная копия будет стёрта при удалении приложения или очистке хранилища. @@ -232,8 +248,12 @@ Группы Название URL (необязательно) + Название фильтра Использовать обновление - Использовать автоматическое обновление + Использовать автообновление + Название предыдущего прокси + Название следующего прокси + Название должно существовать и быть уникальным Обновить подписку группы Проверка профилей группы Время отклика профилей группы @@ -242,21 +262,40 @@ Фильтр групп Все группы Удалено дубликатов профилей: %d + Удалено профилей: %d + Импортировано профилей: %d + Экспортировано профилей: %d + Обновлено профилей: %d Запуск службы Подтвердить - Настройки маршрутизации - Введите требуемые IP/URL через запятую. Не забудьте сохранить изменения. + Доменная стратегия + Маршрутизация + Введите требуемые домены/IP через запятую Сохранить Очистить - Сканировать и заменить - Сканировать и добавить - Правила по умолчанию + Настройка правил маршрутизации + Добавить правило + Импорт набора правил + Существующие правила будут удалены. Продолжить? + Импорт правил из буфера обмена + Импорт правил из QR-кода + Экспорт правил в буфер обмена + Постоянное (сохранится при импорте правил) + Домен + IP + Порт + Протокол + [http,tls,bittorrent] + Сеть + [udp|tcp] + Исходящее подключение Проверить подключение Проверка… - Успешно: HTTP-соединение заняло %d мс + Проверка профилей: %d + Успешно: ping [ %d , %d ] мс Сбой проверки интернет-соединения: %s Интернет недоступен Код ошибки: #%d @@ -282,23 +321,9 @@ Экспорт в буфер обмена - - Проксируемые - Прямые - Блокируемые - - - - Все через прокси - Все, кроме LAN через прокси - Все, кроме Китая через прокси - Все, кроме LAN и Китая через прокси - Все напрямую - - VPN - Только прокси + Прокси @@ -307,4 +332,19 @@ Тёмная + + Белый список Китая + Чёрный список Китая + Общие + Iran lite + Iran medium + Iran heavy + + + + Как в профиле + Пропускает + Не пропускает + + diff --git a/V2rayNG/app/src/main/res/values-vi/strings.xml b/V2rayNG/app/src/main/res/values-vi/strings.xml index 19c2ecfccf..0f29e36947 100644 --- a/V2rayNG/app/src/main/res/values-vi/strings.xml +++ b/V2rayNG/app/src/main/res/values-vi/strings.xml @@ -7,10 +7,12 @@ Đóng Menu ứng dụng Đã chuyển dữ liệu! Không thể chuyển dữ liệu! + Please pull down to refresh! Ngắt kết nối v2rayNG Vui lòng cấp quyền cần thiết cho v2rayNG! Bạn đã từ chối các quyền cần thiết như Camera hay Bộ nhớ? + Unable to obtain the notification permission Nhấn để biết thêm... Đang khởi động v2rayNG... Đã dừng v2rayNG! @@ -24,12 +26,14 @@ Xoá cấu hình Nhập cấu hình từ mã QR Nhập cấu hình từ Clipboard - Nhập thủ công [VMESS] + Nhập thủ công [VMess] Nhập thủ công [VLESS] Nhập thủ công [ShadowSocks] - Nhập thủ công [Socks] + Nhập thủ công [SOCKS] + Type manually[HTTP] Nhập thủ công [Trojan] Nhập thủ công [WireGuard] + Type manually[Hysteria2] Nâng cao / Cấu hình tùy chỉnh Nhập cấu hình tùy chỉnh từ Clipboard Nhập cấu hình tùy chỉnh từ Tệp @@ -51,14 +55,14 @@ http host ws host httpupgrade host - splithttp host + xhttp host h2 host QUIC security gRPC Authority path ws path httpupgrade path - splithttp path + xhttp path h2 path QUIC key kcp seed @@ -77,12 +81,13 @@ Mã hóa Kiểm soát lưu lượng (Flow) PublicKey + PreSharedKey(optional) ShortId SpiderX SecretKey Reserved (Không bắt buộc) Địa chỉ cục bộ (IPv4 / IPv6, phân cách bằng dấu phẩy) - MTU (Không bắt buộc, mặc định là 1420) + MTU (Không bắt buộc, mặc định là 1280) Thành công! Đã xảy ra lỗi, vui lòng thử lại! Không có gì ở đây! @@ -95,14 +100,24 @@ Nội dung Không có dữ liệu nào trong Clipboard! URL không hợp lệ hoặc trống! + Please do not use the insecure HTTP protocol subscription address Vui lòng đảm bảo cấu hình tùy chỉnh này không bị lỗi trước khi sử dụng! Cấu hình không hợp lệ! Host (SNI) (Không bắt buộc) Không thể sao chép tệp tin, hãy dùng trình quản lý tệp! Thêm tệp + Quét mã QR URL Tải xuống tệp tin Hành động này bị cấm! + Obfs password + Port Hopping + Port Hopping Interval + pinSHA256 + Bandwidth down (units) + Bandwidth up (units) + XHTTP Mode + XHTTP Extra raw JSON, format: { XHTTPObject } Thêm URL nội dung @@ -125,6 +140,8 @@ Cài đặt VPN Proxy theo Ứng dụng - Bình thường: Ứng dụng đã chọn sẽ kết nối thông qua Proxy, chưa chọn sẽ kết nối trực tiếp. \n- Chế độ Bypass: Ứng dụng đã chọn sẽ kết nối trực tiếp, chưa chọn sẽ kết nối qua Proxy. \n- Nếu bạn đang ở Trung Quốc thì vào Menu, chọn Tự động chọn ứng dụng Proxy. + Auto connect at startup + Automatically connects to the selected server at startup, which may be unsuccessful Cài đặt Mux Bật Mux @@ -157,19 +174,18 @@ Ưu tiên IPv6 Ưu tiên sử dụng địa chỉ IPv6 cho kết nối và định tuyến. - Định tuyến - Chiến lược tên miền (DomainStrategy) - Quy tắc được định nghĩa trước - Quy tắc tùy chỉnh - DNS ngoại quốc (UDP / TCP / HTTPS / QUIC) (Không bắt buộc) DNS VPN DNS (Chỉ IPv4 / IPv6) + Does VPN bypass LAN DNS nội địa (Không bắt buộc) DNS + DNS hosts (Format: domain:address,…) + domain:address,… + URL kiểm tra độ trễ thực (HTTP / HTTPS) URL @@ -178,11 +194,8 @@ Đang bật cho phép kết nối từ mạng LAN - Cổng Proxy SOCKS5 - Cổng Proxy SOCKS5 - - Cổng Proxy HTTP - Cổng Proxy HTTP + Cổng Proxy Local + Cổng Proxy Local Cổng Local DNS Cổng Local DNS @@ -193,6 +206,9 @@ Quét mã QR ngay lập tức Mở camera để quét mã QR ngay khi khởi động, nếu không, bạn cũng có thể chọn quét mã hoặc chọn ảnh từ thanh công cụ. + Append HTTP Proxy to VPN + HTTP proxy will be used directly from (browser/ some supported apps), without going through the virtual NIC device (Android 10+) + Phản hồi lỗi Phản hồi cải tiến hoặc lỗi lên GitHub Tham gia nhóm Telegram @@ -201,6 +217,7 @@ Chính sách bảo mật Giới thiệu Mã nguồn + Open Source licenses Kênh Telegram Sao lưu cấu hình Nơi lưu trữ: [%s], bản backup sẽ được dọn dẹp sau khi xóa ứng dụng hoặc xóa bộ nhớ. @@ -231,8 +248,12 @@ Các gói đăng ký Tên gói đăng ký URL gói đăng ký + Remarks regular filter Sử dụng gói đăng ký này Bật tự động cập nhật + Previous proxy remarks + Next proxy remarks + The remarks exists and is unique Cập nhật các gói đăng ký Ping tất cả máy chủ Kiểm tra HTTP tất cả máy chủ @@ -242,20 +263,31 @@ Hiển thị tất cả các gói đăng ký Xoá %d cấu hình trùng lặp + Delete %d configurations + Import %d configurations + Export %d configurations + Update %d configurations Khởi động v2rayNG Xác nhận + Chiến lược tên miền (DomainStrategy) Cài đặt định tuyến Phân cách bằng dấu phẩy (,). Có thể tải xuống Rules mặc định để tham khảo ở menu ba chấm. Lưu lại Xoá - Quét QR và Thay thế - Quét QR và Nối thêm - Tải xuống Rules mặc định cho Trung Quốc + Routing Rule Settings + Add rule + Nhập các bộ quy tắc được xác định trước + Existing rulesets will be deleted, are you sure to continue? + Import ruleset from clipboard + Nhập bộ quy tắc từ QRcode + Export ruleset to clipboard + Locked, keep this rule when import presets Kiểm tra kết nối Đang kiểm tra kết nối mạng... - Kiểm tra thành công: thời gian truy cập Google là %d ms + Testing %d configurations… + Success: ping [ %d , %d ] ms Lỗi kết nối mạng, hãy thử đổi cấu hình hoặc kiểm tra lại! Mã lỗi: %s Không có kết nối mạng! Mã lỗi: #%d @@ -276,20 +308,6 @@ Xuất gói vào Clipboard - - Proxy - Direct - Blocked - - - - Proxy toàn cầu - Bỏ qua địa chỉ LAN rồi Proxy - Bỏ qua địa chỉ nội địa rồi Proxy - Bỏ qua LAN và địa chỉ nội địa rồi Proxy - Kết nối trực tiếp toàn cầu - - Chế độ VPN Chế độ Proxy @@ -308,4 +326,10 @@ Fragment Interval (min-max) Enable Fragment + + Follow config + Bypass + Not Bypass + + diff --git a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml index 45b9819427..5616a0771a 100644 --- a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml +++ b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml @@ -7,10 +7,12 @@ Close navigation drawer 数据迁移成功! 数据迁移失败啦! + 请下拉刷新! 停止 无法取得权限 + 无法取得通知权限 点击了解更多 启动服务中 关闭中 @@ -24,12 +26,14 @@ 删除配置 扫描二维码 从剪贴板导入 - 手动输入[Vmess] + 手动输入[VMess] 手动输入[VLESS] 手动输入[Shadowsocks] - 手动输入[Socks] + 手动输入[SOCKS] + 手动输入[HTTP] 手动输入[Trojan] 手动输入[Wireguard] + 手动输入[Hysteria2] 自定义配置 从剪贴板导入自定义配置 从本地导入自定义配置 @@ -51,14 +55,14 @@ http host ws host httpupgrade host - splithttp host + xhttp host h2 host QUIC 加密方式 gRPC Authority path ws path httpupgrade path - splithttp path + xhttp path h2 path QUIC 加密密钥 kcp seed @@ -77,12 +81,13 @@ 加密方式(encryption) 流控(flow) PublicKey + PreSharedKey(optional) ShortId SpiderX SecretKey - Reserved(可选) + Reserved(可选,逗号隔开) 本地地址(可选IPv4/IPv6,逗号隔开) - Mtu(可选, 默认1420) + Mtu(可选, 默认1280) 成功 失败 没有数据 @@ -95,15 +100,24 @@ 内容 剪贴板中没有数据 无效的网址 + 请不要使用不安全的HTTP协议订阅地址 确保inbounds port和设置中的一致 配置格式错误 Host(SNI)(可选) 失败, 请使用文件管理器 添加文件 + 扫描 QRcode URL 下载文件 禁止此项操作 - + 混淆密码 + 跳跃端口(会覆盖服务器端口) + 端口跳跃间隔(秒) + SHA256证书指纹 + 带宽下行 (单位) + 带宽上行 (单位) + XHTTP 模式 + XHTTP Extra 原始 JSON,格式: { XHTTPObject } 添加资产网址 @@ -125,6 +139,8 @@ VPN 设置 分应用代理 常规:勾选的App被代理,未勾选的直连;\n绕行模式:勾选的App直连,未勾选的被代理.\n不明白者在菜单中选择自动选中需代理应用 + 开机时自动连接 + 开机时自动连接选择的服务器,可能会不成功 Mux 多路复用 设置 启用 Mux 多路复用 @@ -155,19 +171,18 @@ IPv6优先 App优先使用IPv6地址连接服务器,同时开启VPN的IPv6路由 - 路由设置 - 域名策略 - 预定义规则 - 自定义规则 - 远程DNS (udp/tcp/https/quic)(可选) DNS VPN DNS (仅支持 IPv4/v6) + VPN是否绕过局域网 境内DNS (可选) DNS + DNS hosts (格式: 域名:地址,…) + domain:address,… + 真连接延迟测试网址 (http/https) Url @@ -176,11 +191,8 @@ 允许来自局域网的连接,请确保处于受信网络 - SOCKS5代理端口 - SOCKS5代理端口 - - HTTP代理端口 - HTTP代理端口 + 本地代理端口 + 本地代理端口 本地DNS端口 本地DNS端口 @@ -191,6 +203,9 @@ 立即启动扫码 启动时立即打开相机扫描,否则可在工具栏选择扫码或选照片 + 追加 HTTP 代理至 VPN + 浏览器 / 一些支持的应用 将直接使用 HTTP 代理, 而不经过虚拟网卡设备 (Android 10+) + 反馈 反馈改进或漏洞至 GitHub 加入Telegram Group @@ -198,6 +213,7 @@ 隐私权政策 关于 源代码 + Open Source licenses Telegram 频道 备份配置 存储位置: [%s], 卸载App或清除存储后备份将被清除 @@ -229,8 +245,12 @@ 订阅分组设置 备注 可选地址(url) + 别名正则过滤 启用更新 启用自动更新 + 前置代理别名 + 落地代理別名 + 请确保别名存在并唯一 更新当前组订阅 测试当前组配置Tcping 测试当前组配置真连接 @@ -240,20 +260,31 @@ 所有分组 删除 %d 个重复配置 + 删除 %d 个配置 + 导入 %d 个配置 + 导出 %d 个配置 + 更新 %d 个配置 启动服务 确定 + 域名策略 路由设置 - 用逗号(,)隔开,可以一行多个,记得保存 + 用逗号(,)隔开,domain和ip二选一填写 保存 清空 - 扫描并替换 - 扫描并追加 - 设置默认路由规则 + 路由规则设置 + 添加规则 + 导入预定义规则集 + 将删除现有的规则集,是否确定继续? + 从剪贴板导入规则集 + 从 QRcode 导入规则集 + 导出规则集至剪贴板 + 锁定中,导入预设时不删除此规则 "检查网络连接" "测试中…" - "连接成功:延时 %d 毫秒" + 测试 %d 个配置中… + "连接成功:延时 [ %d , %d ] 毫秒" "失败:%s" "无互联网连接" "状态码无效(#%d)" @@ -281,21 +312,6 @@ 导出至剪贴板 - share_method - - 代理的网址或IP - 直连的网址或IP - 阻止的网址或IP - - - - 全局代理 - 绕过局域网地址而后代理 - 绕过大陆地址而后代理 - 绕过局域网及大陆地址而后代理 - 全局直连 - - VPN 仅代理 @@ -307,4 +323,19 @@ 深色 + + 绕过大陆(Whitelist) + 黑名单(Blacklist) + 全局(Global) + Iran lite + Iran medium + Iran heavy + + + + 跟随配置文件 + 绕过 + 不绕过 + + diff --git a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml index a95ce8fe23..c56c18baa3 100644 --- a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml +++ b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml @@ -7,10 +7,12 @@ 關閉導覽匣 資料遷移成功! 資料遷移失敗! + 請下拉刷新! 停止 無法取得此權限 + 無法取得此通知權限 瞭解更多 啟動服務 停止服務 @@ -18,23 +20,25 @@ 啟動服務失敗 - 配置檔案 - 新增配置 - 儲存配置 - 刪除配置 - 從 QR Code 匯入配置 - 從剪貼簿匯入配置 - 手動鍵入 [Vmess] + 設定檔 + 新增設定 + 儲存設定 + 刪除設定 + 從 QR Code 匯入設定 + 從剪貼簿匯入設定 + 手動鍵入 [VMess] 手動鍵入 [VLESS] 手動鍵入 [Shadowsocks] - 手動鍵入 [Socks] + 手動鍵入 [SOCKS] + 手動鍵入 [HTTP] 手動鍵入 [Trojan] 手動鍵入 [Wireguard] - 自訂配置 - 從剪貼簿匯入自訂配置 - 從本地匯入自訂配置 - 從 URL 匯入自訂配置 - 掃描 URL 匯入自訂配置 + 手動鍵入 [Hysteria2] + 自訂設定 + 從剪貼簿匯入自訂設定 + 從本地匯入自訂設定 + 從 URL 匯入自訂設定 + 掃描 URL 匯入自訂設定 確定刪除? 刪除前請先測試!確認刪除? 備註 @@ -51,14 +55,14 @@ http host ws host httpupgrade host - splithttp host + xhttp host h2 host QUIC 加密方式 gRPC Authority path ws path httpupgrade path - splithttp path + xhttp path h2 path QUIC 加密金鑰 kcp seed @@ -77,32 +81,43 @@ 加密 (encryption) 流程 (flow) PublicKey + PreSharedKey(optional) ShortId SpiderX SecretKey - Reserved (可選) + Reserved (可選,逗號隔開) 本機位址(可選IPv4/IPv6,逗號隔開) - MTU(可選, 預設1420) + MTU(可選, 預設1280) 成功 失敗 無資料 通訊協定不正確 解碼失敗 - 選取一個配置檔 + 選取一個設定檔 請安裝檔案總管。 - 自訂配置 - 無效配置 + 自訂設定 + 無效設定 內容 剪貼簿內無資料 URL 無效 + 請不要使用不安全的HTTP協定訂閱位址 ​​確保 inbounds port 和設定中的一致 - 配置格式不正確 + 設定格式不正確 Host(SNI)(可選) 失敗,請使用檔案總管 新增檔案 + 掃描 QRcode URL 下載檔案 禁止此項操作 + 混淆密碼 + 跳躍連接埠(會覆蓋伺服器連接埠) + 連接埠跳躍間隔(秒) + SHA256憑證指紋 + 頻寬下行 (單位) + 頻寬上行 (單位) + XHTTP 模式 + XHTTP Extra 原始 JSON,格式: { XHTTPObject } 新增資產網址 @@ -125,6 +140,8 @@ VPN 設定 Proxy 個別應用程式 常規:勾選的 App 啟用 Proxy,未勾選的直接連線;\n繞行模式:勾選的 App 直接連線,未勾選的啟用 Proxy。\n可在選單中選擇自動選中需 Proxy 應用 + 開機時自動連線 + 開機時自動連線選擇的伺服器,可能會不成功 Mux 設定 啟用 Mux 多路復用 @@ -156,18 +173,14 @@ IPv6 偏好 App 優先使用 IPv6 位址連線伺服器,同时開啟 VPN 的 IPv6 路由 - 轉送設定 - 網域策略 - 轉送模式 - 自訂轉送 - 遠端DNS (udp/tcp/https/quic)(可選) DNS VPN DNS (僅支援 IPv4/v6) + VPN是否繞過區域網 - 國內 DNS (可選) - DNS + DNS hosts (格式: 網域:位址,…) + domain:address,… 真連線延遲測試網址 (http/https) Url @@ -177,21 +190,21 @@ 允許來自區域網路的連線,請確保處於受信網路 - SOCKS5 Proxy 埠 - SOCKS5 Proxy 埠 - - HTTP Proxy 埠 - HTTP Proxy 埠 + 本地 Proxy 埠 + 本地 Proxy 埠 本機 DNS 埠 本機 DNS 埠 - 刪除配置檔案確認 - 刪除配置檔案是否需要用戶二次確認 + 刪除設定檔確認 + 刪除設定檔是否需要用戶二次確認 立即啟動掃碼 啟動時立即打開相機掃描,否則可在工具欄選擇掃碼或選照片 + 追加 HTTP 代理至 VPN + 瀏覽器 / 一些支援的應用 將直接使用 HTTP 代理, 而不經過虛擬網卡設備 (Android 10+) + 意見回饋 前往 GitHub 回報錯誤 加入 Telegram 群組 @@ -199,11 +212,12 @@ 隱私權政策 關於 原始碼 + Open Source licenses Telegram 頻道 - 備份配置 + 備份設定 儲存位置: [%s], 卸載App或清除儲存後備份將被清除 - 還原配置 - 分享配置 + 還原設定 + 分享設定 推廣 一些推廣,輕觸以檢視 (捐贈可去除) @@ -223,38 +237,53 @@ 複製 清除 重啟服務 - 刪除目前群組配置 - 刪除目前群組重複配置 - 刪除目前群組無效配置 - 匯出目前群組配置至剪貼簿 + 刪除目前群組設定 + 刪除目前群組重複設定 + 刪除目前群組無效設定 + 匯出目前群組設定至剪貼簿 訂閱分組設定 備註 - Optional URL + 可選位址(url) + 別名正規過濾 啟用更新 啟用自動更新 + 前置代理别名 + 落地代理別名 + 请确保别名存在并唯一 更新目前群組訂閱 - 偵測目前群組配置 Tcping - 偵測目前群組配置真延遲 + 偵測目前群組設定 Tcping + 偵測目前群組設定真延遲 Geo 資源檔案 依偵測結果排序 - 過濾配置 + 過濾設定 所有分組 - Delete %d duplicate configurations + 删除 %d 个重复配置 + 删除 %d 个配置 + 导入 %d 个配置 + 导出 %d 个配置 + 更新 %d 个配置 啟動服務 確定 + 網域策略 轉送設定 - 以半形逗號「,」分隔,並手動儲存 + 以半形逗號「,」分隔,domain和ip二選一填寫 儲存 清除 - 掃描並取代 - 掃描並附加 - 設定預設轉送規則 + 路由規則設定 + 新增規則 + 匯入預先定義的規則集 + 將刪除現有的規則集,是否確定繼續? + 從剪貼簿匯入規則集 + 從 QRcode 匯入規則集 + 匯出規則集至剪貼簿 + 鎖定中,匯入預設時不刪除此規則 "測試連線能力" "測試中……" - "成功:%d ms延遲" + 測試 %d 个配置中… + "成功:[ %d , %d ] ms延遲" "測試網際網路連線失敗:%s" "無法使用網際網路" "錯誤碼:(#%d)" @@ -274,7 +303,7 @@ QR Code 匯出至剪貼簿 - 匯出完整配置至剪貼簿 + 匯出完整設定至剪貼簿 @@ -282,20 +311,6 @@ 匯出至剪貼簿 - - Proxy URL 或 IP - 直接連線 URL 或 IP - 已封鎖的 URL 或 IP - - - - 全域 Proxy - 略過區域網路的 Proxy - 略過中國大陸的 Proxy - 略過區域網路及中國大陸的 Proxy - 直接連線 - - VPN 僅 Proxy @@ -307,4 +322,19 @@ 深色 + + 繞過大陸(Whitelist) + 黑名單(Blacklist) + 全域(Global) + Iran lite + Iran medium + Iran heavy + + + + 跟隨設定檔 + 繞過 + 不繞過 + + diff --git a/V2rayNG/app/src/main/res/values/arrays.xml b/V2rayNG/app/src/main/res/values/arrays.xml index 7b8580bf4f..9437834964 100644 --- a/V2rayNG/app/src/main/res/values/arrays.xml +++ b/V2rayNG/app/src/main/res/values/arrays.xml @@ -26,7 +26,7 @@ kcp ws httpupgrade - splithttp + xhttp h2 quic grpc @@ -100,14 +100,6 @@ false - - 0 - 1 - 2 - 3 - 4 - - AsIs IPIfNonMatch @@ -180,6 +172,7 @@ فارسی عربي বাংলা + لۊری بختیاری @@ -193,6 +186,7 @@ fa ar bn + bqi-rIR @@ -206,4 +200,24 @@ 1 2 + + + proxy + direct + block + + + + auto + packet-up + stream-up + stream-one + + + + 0 + 1 + 2 + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/values/dimens.xml b/V2rayNG/app/src/main/res/values/dimens.xml index d082409aae..84a8a1800a 100644 --- a/V2rayNG/app/src/main/res/values/dimens.xml +++ b/V2rayNG/app/src/main/res/values/dimens.xml @@ -7,7 +7,12 @@ 50dp 24dp 72dp + 90dp 60dp + 16dp + 16dp + 16dp + 3dp 16dp 16dp diff --git a/V2rayNG/app/src/main/res/values/strings.xml b/V2rayNG/app/src/main/res/values/strings.xml index 43e4e8e542..46187b1d72 100644 --- a/V2rayNG/app/src/main/res/values/strings.xml +++ b/V2rayNG/app/src/main/res/values/strings.xml @@ -8,10 +8,12 @@ Close navigation drawer Data migration success! Data migration failed! + Please pull down to refresh! Stop Unable to obtain the permission + Unable to obtain the notification permission click for more Start Services Stop Services @@ -25,12 +27,14 @@ Delete config Import config from QRcode Import config from Clipboard - Type manually[Vmess] + Type manually[VMess] Type manually[VLESS] Type manually[Shadowsocks] - Type manually[Socks] + Type manually[SOCKS] + Type manually[HTTP] Type manually[Trojan] Type manually[Wireguard] + Type manually[Hysteria2] Custom config Import custom config from Clipboard Import custom config from locally @@ -52,14 +56,14 @@ http host ws host httpupgrade host - splithttp host + xhttp host h2 host QUIC security gRPC Authority path ws path httpupgrade path - splithttp path + xhttp path h2 path QUIC key kcp seed @@ -77,15 +81,14 @@ User(Optional) encryption flow - - PublicKey - ShortId - SpiderX - Secret or PrivateKey - - Reserved(Optional) + PublicKey + PreSharedKey(optional) + ShortId + SpiderX + Secret or PrivateKey + Reserved(Optional, separated by commas) Local address (optional IPv4/IPv6, separated by commas) - Mtu(optional, default 1420) + Mtu(optional, default 1280) Success Failure There is nothing @@ -98,6 +101,7 @@ Content There is no data in the clipboard Invalid URL + Please do not use the insecure HTTP protocol subscription address Ensure inbounds port is consistent with the settings Config malformed Host(SNI)(Optional) @@ -105,13 +109,21 @@ Add asset Add files Add URL + Scan QRcode URL Download files Add asset URL File not found The remarks already exists Action not allowed - + Obfs password + Port Hopping(will override the port) + Port Hopping Interval + pinSHA256 + Bandwidth down (units) + Bandwidth up (units) + XHTTP Mode + XHTTP Extra raw JSON, format: { XHTTPObject } Loading @@ -131,6 +143,8 @@ VPN Settings Per-app proxy General: Checked App is proxy, unchecked direct connection; \nbypass mode: checked app directly connected, unchecked proxy. \nThe option to automatically select the proxy application in the menu + Auto connect at startup + Automatically connects to the selected server at startup, which may be unsuccessful Mux Settings Enable Mux @@ -163,34 +177,30 @@ Prefer IPv6 Prefer IPv6 address and routes - Routing - Domain strategy - Predefined rules - Custom rules - Remote DNS (udp/tcp/https/quic)(Optional) DNS VPN DNS (only IPv4/v6) + Does VPN bypass LAN Domestic DNS (Optional) DNS + DNS hosts (Format: domain:address,…) + domain:address,… + True delay test url (http/https) Url Allow connections from the LAN - Other devices can connect to proxy by your ip address through socks/http, Only enable in trusted network to avoid unauthorized connection + Other devices can connect to proxy by your ip address through local proxy, Only enable in trusted network to avoid unauthorized connection Allow connections from the LAN, Make sure you are in a trusted network Disable Allow Insecure Verify config\'s cert, Don\'t allow self-signed config - SOCKS5 proxy port - SOCKS5 proxy port - - HTTP proxy port - HTTP proxy port + Local proxy port + Local proxy port Local DNS port Local DNS port @@ -201,6 +211,9 @@ Start scanning immediately Open the camera to scan immediately at startup, otherwise you can choose to scan the code or select a photo in the toolbar + Append HTTP Proxy to VPN + HTTP proxy will be used directly from (browser/ some supported apps), without going through the virtual NIC device (Android 10+) + Feedback Feedback enhancements or bugs to GitHub Join Telegram Group @@ -208,6 +221,7 @@ Privacy policy About Source code + Open Source licenses Telegram channel Backup configuration Storage location: [%s], The backup will be cleared after uninstalling the app or clearing the storage @@ -239,8 +253,12 @@ Subscription group setting remarks Optional URL + Remarks regular filter Enable update Enable automatic update + Previous proxy remarks + Next proxy remarks + The remarks exists and is unique Update current group subscription Tcping current group configuration Real delay current group configuration @@ -249,21 +267,40 @@ Filter configuration file All groups Delete %d duplicate configurations + Delete %d configurations + Import %d configurations + Export %d configurations + Update %d configurations Start Service Confirm + Domain strategy Routing Settings - Separated by commas(,),remember to save + Separated by commas(,),choose domain or ip Save Clear - Scan and replace - Scan and append - set default routing rules + Routing Rule Settings + Add rule + Restore Default Rules … + Existing rulesets will be deleted, are you sure to continue? + Import ruleset from clipboard + Import ruleset from QRcode + Export ruleset to clipboard + Locked, keep this rule when import presets + domain + ip + port + protocol + [http,tls,bittorrent] + network"[udp|tcp]" + [udp|tcp] + outboundTag Check Connectivity Testing… - Success: HTTP connection took %dms + Testing %d configurations… + Success: ping [ %d , %d ] ms Fail to detect internet connection: %s Internet Unavailable Error code: #%d @@ -289,20 +326,6 @@ Export to clipboard - - proxy URL or IP - direct URL or IP - blocked URL or IP - - - - Global proxy - Bypassing the LAN address then proxy - Bypass mainland address then proxy - Bypassing LAN and mainland address then proxy - Global direct - - VPN Proxy only @@ -314,4 +337,19 @@ Dark + + China Whitelist + China Blacklist + Global + Iran lite + Iran medium + Iran heavy + + + + Follow config + Bypass + Not Bypass + + diff --git a/V2rayNG/app/src/main/res/values/styles.xml b/V2rayNG/app/src/main/res/values/themes.xml similarity index 89% rename from V2rayNG/app/src/main/res/values/styles.xml rename to V2rayNG/app/src/main/res/values/themes.xml index aae3aa7334..d6558e07e6 100644 --- a/V2rayNG/app/src/main/res/values/styles.xml +++ b/V2rayNG/app/src/main/res/values/themes.xml @@ -14,7 +14,7 @@ - + + + diff --git a/V2rayNG/app/src/main/res/xml/pref_settings.xml b/V2rayNG/app/src/main/res/xml/pref_settings.xml index 4210d3b7bd..6b644b962b 100644 --- a/V2rayNG/app/src/main/res/xml/pref_settings.xml +++ b/V2rayNG/app/src/main/res/xml/pref_settings.xml @@ -13,6 +13,10 @@ android:key="pref_route_only_enabled" android:summary="@string/summary_pref_route_only_enabled" android:title="@string/title_pref_route_only_enabled" /> + + + - - - - - - - + android:title="@string/title_pref_vpn_bypass_lan" /> @@ -197,12 +190,6 @@ android:summary="10808" android:title="@string/title_pref_socks_port" /> - - + + \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +64,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,28 +75,14 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -85,7 +106,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -105,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -134,27 +156,30 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/V2rayNG/gradlew.bat b/V2rayNG/gradlew.bat index 8a0b282aa6..107acd32c4 100644 --- a/V2rayNG/gradlew.bat +++ b/V2rayNG/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -8,20 +24,23 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,34 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/V2rayNG/settings.gradle.kts b/V2rayNG/settings.gradle.kts index f86c01148e..7ac30b30b1 100644 --- a/V2rayNG/settings.gradle.kts +++ b/V2rayNG/settings.gradle.kts @@ -1,8 +1,14 @@ pluginManagement { repositories { - gradlePluginPortal() - google() + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } mavenCentral() + gradlePluginPortal() } } dependencyResolutionManagement { @@ -10,9 +16,9 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - jcenter() maven { url = uri("https://jitpack.io") } } } -rootProject.name = "V2rayNG" + +rootProject.name = "v2rayNG" include(":app") diff --git a/compile-tun2socks.sh b/compile-tun2socks.sh new file mode 100644 index 0000000000..11bea0d247 --- /dev/null +++ b/compile-tun2socks.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -o errexit +set -o pipefail +set -o nounset +# Set magic variables for current file & dir +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__file="${__dir}/$(basename "${BASH_SOURCE[0]}")" +__base="$(basename ${__file} .sh)" +if [[ ! -d $NDK_HOME ]]; then + echo "Android NDK: NDK_HOME not found. please set env \$NDK_HOME" + exit 1 +fi +TMPDIR=$(mktemp -d) +clear_tmp () { + rm -rf $TMPDIR +} +trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; trap ERR; clear_tmp; exit 1' ERR INT +install -m644 $__dir/tun2socks.mk $TMPDIR/ +pushd $TMPDIR +ln -s $__dir/badvpn badvpn +ln -s $__dir/libancillary libancillary +$NDK_HOME/ndk-build \ + NDK_PROJECT_PATH=. \ + APP_BUILD_SCRIPT=./tun2socks.mk \ + APP_ABI=all \ + APP_PLATFORM=android-19 \ + NDK_LIBS_OUT=$TMPDIR/libs \ + NDK_OUT=$TMPDIR/tmp \ + APP_SHORT_COMMANDS=false LOCAL_SHORT_COMMANDS=false -B -j4 \ + LOCAL_LDFLAGS=-Wl,--build-id=none +tar cvfz $__dir/libtun2socks.so.tgz libs +popd +rm -rf $TMPDIR diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt new file mode 100644 index 0000000000..34267c99c3 --- /dev/null +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -0,0 +1,27 @@ +

A V2Ray client for Android, support Xray core and v2fly core

+ +

Telegram Channel

+ +

github_2dust

+ +

Usage

+ +

Geoip and Geosite

+ +
    +
  • geoip.dat and geosite.dat files are in Android/data/com.v2ray.ang/files/assets (path may differ on some Android device)
  • +
  • download feature will get enhanced version in this repo (Note it need a working proxy)
  • +
  • latest official domain list and ip list can be imported manually
  • +
  • possible to use third party dat file in the same folder, like h2y
  • +
+ +

More in our wiki

+ +

Development guide

+ +

Android project under V2rayNG folder can be compiled directly in Android Studio, or using Gradle wrapper. But the v2ray core inside the aar is (probably) outdated. +The aar can be compiled from the Golang project AndroidLibV2rayLite or AndroidLibXrayLite. +For a quick start, read guide for Go Mobile and Makefiles for Go Developers

+ +

v2rayNG can run on Android Emulators. For WSA, VPN permission need to be granted via +appops set [package name] ACTIVATE_VPN allow

diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 0000000000..03a4ce8a4a Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt new file mode 100644 index 0000000000..38a05c70c7 --- /dev/null +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -0,0 +1 @@ +A V2Ray client for Android, support Xray core and v2fly core diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt new file mode 100644 index 0000000000..84cf2a177f --- /dev/null +++ b/fastlane/metadata/android/en-US/title.txt @@ -0,0 +1 @@ +v2rayNG diff --git a/libhysteria2.sh b/libhysteria2.sh new file mode 100644 index 0000000000..b2a721170d --- /dev/null +++ b/libhysteria2.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +targets=( + "aarch64-linux-android21 arm64 arm64-v8a" + "armv7a-linux-androideabi21 arm armeabi-v7a" + "x86_64-linux-android21 amd64 x86_64" + "i686-linux-android21 386 x86" +) + +cd "hysteria" || exit + +for target in "${targets[@]}"; do + IFS=' ' read -r ndk_target goarch abi <<< "$target" + + echo "Building for ${abi} with ${ndk_target} (${goarch})" + + CC="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/${ndk_target}-clang" CGO_ENABLED=1 CGO_LDFLAGS="-Wl,-z,max-page-size=16384" GOOS=android GOARCH=$goarch go build -o libs/$abi/libhysteria2.so -trimpath -ldflags "-s -w -buildid=" -buildvcs=false ./app + + echo "Built libhysteria2.so for ${abi}" +done diff --git a/tun2socks.mk b/tun2socks.mk new file mode 100644 index 0000000000..04ac8df32e --- /dev/null +++ b/tun2socks.mk @@ -0,0 +1,124 @@ +# Copyright (C) 2009 The Android Open Source Project +# +# 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. +# +# +LOCAL_PATH := $(call my-dir) +ROOT_PATH := $(LOCAL_PATH) +######################################################## +## libancillary +######################################################## +include $(CLEAR_VARS) +ANCILLARY_SOURCE := fd_recv.c fd_send.c +LOCAL_MODULE := libancillary +#LOCAL_CFLAGS += -I$(LOCAL_PATH)/libancillary +LOCAL_C_INCLUDES := $(LOCAL_PATH)/libancillary +LOCAL_SRC_FILES := $(addprefix libancillary/, $(ANCILLARY_SOURCE)) +include $(BUILD_STATIC_LIBRARY) +######################################################## +## tun2socks +######################################################## +include $(CLEAR_VARS) +LOCAL_CFLAGS := -std=gnu99 +LOCAL_CFLAGS += -DBADVPN_THREADWORK_USE_PTHREAD -DBADVPN_LINUX -DBADVPN_BREACTOR_BADVPN -D_GNU_SOURCE +LOCAL_CFLAGS += -DBADVPN_USE_SIGNALFD -DBADVPN_USE_EPOLL +LOCAL_CFLAGS += -DBADVPN_LITTLE_ENDIAN -DBADVPN_THREAD_SAFE +LOCAL_CFLAGS += -DNDEBUG -DANDROID +LOCAL_CFLAGS += -I +LOCAL_STATIC_LIBRARIES := libancillary +LOCAL_C_INCLUDES := \ + $(LOCAL_PATH)/badvpn/libancillary \ + $(LOCAL_PATH)/badvpn/lwip/src/include/ipv4 \ + $(LOCAL_PATH)/badvpn/lwip/src/include/ipv6 \ + $(LOCAL_PATH)/badvpn/lwip/src/include \ + $(LOCAL_PATH)/badvpn/lwip/custom \ + $(LOCAL_PATH)/badvpn \ + $(LOCAL_PATH)/libancillary +TUN2SOCKS_SOURCES := \ + base/BLog_syslog.c \ + system/BReactor_badvpn.c \ + system/BSignal.c \ + system/BConnection_common.c \ + system/BConnection_unix.c \ + system/BTime.c \ + system/BUnixSignal.c \ + system/BNetwork.c \ + system/BDatagram_common.c \ + system/BDatagram_unix.c \ + flow/StreamRecvInterface.c \ + flow/PacketRecvInterface.c \ + flow/PacketPassInterface.c \ + flow/StreamPassInterface.c \ + flow/SinglePacketBuffer.c \ + flow/BufferWriter.c \ + flow/PacketBuffer.c \ + flow/PacketStreamSender.c \ + flow/PacketPassConnector.c \ + flow/PacketProtoFlow.c \ + flow/PacketPassFairQueue.c \ + flow/PacketProtoEncoder.c \ + flow/PacketProtoDecoder.c \ + socksclient/BSocksClient.c \ + tuntap/BTap.c \ + lwip/src/core/udp.c \ + lwip/src/core/memp.c \ + lwip/src/core/init.c \ + lwip/src/core/pbuf.c \ + lwip/src/core/tcp.c \ + lwip/src/core/tcp_out.c \ + lwip/src/core/netif.c \ + lwip/src/core/def.c \ + lwip/src/core/ip.c \ + lwip/src/core/mem.c \ + lwip/src/core/tcp_in.c \ + lwip/src/core/stats.c \ + lwip/src/core/inet_chksum.c \ + lwip/src/core/timeouts.c \ + lwip/src/core/ipv4/icmp.c \ + lwip/src/core/ipv4/igmp.c \ + lwip/src/core/ipv4/ip4_addr.c \ + lwip/src/core/ipv4/ip4_frag.c \ + lwip/src/core/ipv4/ip4.c \ + lwip/src/core/ipv4/autoip.c \ + lwip/src/core/ipv6/ethip6.c \ + lwip/src/core/ipv6/inet6.c \ + lwip/src/core/ipv6/ip6_addr.c \ + lwip/src/core/ipv6/mld6.c \ + lwip/src/core/ipv6/dhcp6.c \ + lwip/src/core/ipv6/icmp6.c \ + lwip/src/core/ipv6/ip6.c \ + lwip/src/core/ipv6/ip6_frag.c \ + lwip/src/core/ipv6/nd6.c \ + lwip/custom/sys.c \ + tun2socks/tun2socks.c \ + base/DebugObject.c \ + base/BLog.c \ + base/BPending.c \ + flowextra/PacketPassInactivityMonitor.c \ + tun2socks/SocksUdpGwClient.c \ + udpgw_client/UdpGwClient.c \ + socks_udp_client/SocksUdpClient.c +LOCAL_MODULE := tun2socks +LOCAL_LDLIBS := -ldl -llog +LOCAL_SRC_FILES := $(addprefix badvpn/, $(TUN2SOCKS_SOURCES)) +LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE +LOCAL_MAKEFILE := $(local-makefile) +$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT)) +$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE)) +$(call check-LOCAL_MODULE_FILENAME) +# we are building target objects +my := TARGET_ +$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION)) +$(call handle-module-built) +LOCAL_MODULE_CLASS := EXECUTABLE +include $(BUILD_SYSTEM)/build-module.mk