diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 8c5a92fe..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,159 +0,0 @@ -name: Build APK - -on: - workflow_dispatch: - inputs: - release_tag: - required: false - type: string - push: - branches: - - master - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4.2.2 - with: - submodules: 'recursive' - fetch-depth: '0' - - - name: Setup Android SDK - uses: android-actions/setup-android@v3.2.0 - with: - log-accepted-android-sdk-licenses: false - cmdline-tools-version: '12266719' - packages: 'platforms;android-35 build-tools;35.0.0 platform-tools' - - - name: Install NDK - run: | - echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \ - --channel=3 \ - --install "ndk;29.0.13113456" - echo "NDK_HOME=$ANDROID_HOME/ndk/29.0.13113456" >> $GITHUB_ENV - sed -i '10i\ - \ - ndkVersion = "29.0.13113456"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts - - - name: Restore cached libtun2socks - id: cache-libtun2socks-restore - uses: actions/cache/restore@v4 - with: - path: ${{ github.workspace }}/libs - key: libtun2socks-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }} - - - name: Build libtun2socks - if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true' - run: | - bash compile-tun2socks.sh - - - 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 }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }} - - - 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.12 - 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 }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }} - - - name: Setup Golang - if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true' - uses: actions/setup-go@v5.4.0 - with: - go-version-file: 'AndroidLibXrayLite/go.mod' - cache: false - - - name: Build libhysteria2 - if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true' - run: | - bash libhysteria2.sh - - - name: Save libhysteria2 - if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 - with: - path: ${{ github.workspace }}/hysteria/libs - key: libhysteria2-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }} - - - name: Copy libhysteria2 - run: | - cp -r ${{ github.workspace }}/hysteria/libs ${{ github.workspace }}/V2rayNG/app - - - name: Setup Java - uses: actions/setup-java@v4.7.0 - with: - distribution: 'temurin' - java-version: '21' - - - name: Decode Keystore - uses: timheuer/base64-to-file@v1.2.4 - id: android_keystore - with: - fileName: "android_keystore.jks" - encodedString: ${{ secrets.APP_KEYSTORE_BASE64 }} - - - name: Build APK - run: | - cd ${{ github.workspace }}/V2rayNG - echo "sdk.dir=${ANDROID_HOME}" > local.properties - chmod 755 gradlew - ./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 }} - - - name: Upload arm64-v8a APK - uses: actions/upload-artifact@v4.6.2 - 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.6.2 - 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.6.2 - 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: - 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 deleted file mode 100644 index 4ed66e23..00000000 --- a/.github/workflows/fastlane.yml +++ /dev/null @@ -1,16 +0,0 @@ -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.1.0 diff --git a/.gitignore b/.gitignore index 9582b273..0963c509 100644 --- a/.gitignore +++ b/.gitignore @@ -1,66 +1,5 @@ -# Ignore data and key store files *.dat *.jks - -# Ignore output JSON file V2rayNG/app/release/output.json - -# Ignore IDE and build system directories .idea/ .gradle/ -*.iml - -# Ignore local properties and DS_Store files -/local.properties -.DS_Store - -# Ignore build directories and captures -/build -/captures -V2rayNG/app/build -V2rayNG/build -V2rayNG/local.properties - -# Ignore APK and AAR files -*.apk -*.aar - -# Ignore signing properties -signing.properties - -# Ignore shared object files -*.so - -# Ignore Google services JSON -V2rayNG/app/google-services.json - -# Additional common Android/Java ignores -*.log -*.tmp -*.bak -*.swp -*.orig -*.class -*.jar -*.war -*.ear - -# Ignore executable files -*.exe -*.dll -*.obj -*.o -*.pyc -*.pyo - -# Ignore files from other IDEs -.vscode/ -.classpath -.project -.settings/ -*.sublime-workspace -*.sublime-project - -# Ignore OS-specific files -Thumbs.db -.DS_Store diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 2bdafca3..00000000 --- a/.gitmodules +++ /dev/null @@ -1,12 +0,0 @@ -[submodule "hysteria"] - path = hysteria - url = https://github.com/apernet/hysteria -[submodule "AndroidLibXrayLite"] - path = AndroidLibXrayLite - url = https://github.com/2dust/AndroidLibXrayLite -[submodule "badvpn"] - path = badvpn - url = https://github.com/XTLS/badvpn -[submodule "libancillary"] - path = libancillary - url = https://github.com/shadowsocks/libancillary diff --git a/AndroidLibV2rayLite/README.md b/AndroidLibV2rayLite/README.md new file mode 100644 index 00000000..118eefcf --- /dev/null +++ b/AndroidLibV2rayLite/README.md @@ -0,0 +1,20 @@ +# 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/AndroidLibXrayLite b/AndroidLibXrayLite deleted file mode 160000 index 8ad3e1dd..00000000 --- a/AndroidLibXrayLite +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8ad3e1ddf165d8d67e488346b2faa9153d3e33a4 diff --git a/CR.md b/CR.md index b115f898..0ec148b6 100644 --- a/CR.md +++ b/CR.md @@ -1,40 +1,29 @@ -**v2rayNG 隐私权政策** +v2rayNG 隐私条款 -本政策自2023年11月17日起施行 +最后更新 2017-11-22 -2dust 将 v2rayNG 应用程序构建为开源应用程序。 本服务由 2dust 免费提供,并且旨在按原样使用。 +v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私条款。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。** -v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私权政策。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。** +1. 信息收集 -**信息收集** + v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私条款。 -v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私权政策。 + v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。 -v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。 + 当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。** -当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。** +2. 信息共享 -**信息共享** + 我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。 -我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。 +3. 信息存留 -**信息存留** + 除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。 -除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。 +4. 信息泄露 -**信息泄露** + 我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。** -我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。** - -**儿童隐私** - -这些服务不针对 13 岁以下的任何人。我不会故意收集 13 岁以下儿童的个人身份信息。 如果我发现 13 岁以下的儿童向我提供了个人信息,我会立即从我们的服务器中删除该信息。 如果您是父母或监护人,并且您知道您的孩子向我们提供了个人信息,请与我联系,以便我能够采取必要的行动。 - -**条款修改** - -我们保留修改这份隐私权政策的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私权政策。** - -**联系我们** - -如果您对我的隐私政策有任何疑问或建议,请随时通过 CaptainIronng@protonmail.com 与我联系。 +5. 条款修改 + 我们保留修改这份隐私条款的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。** diff --git a/README.md b/README.md index 4bd6f8ec..34c837dc 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,16 @@ A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core) [![API](https://img.shields.io/badge/API-21%2B-yellow.svg?style=flat)](https://developer.android.com/about/versions/lollipop) -[![Kotlin Version](https://img.shields.io/badge/Kotlin-2.1.21-blue.svg)](https://kotlinlang.org) +[![Kotlin Version](https://img.shields.io/badge/Kotlin-1.6.21-blue.svg)](https://kotlinlang.org) [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayNG)](https://github.com/2dust/v2rayNG/commits/master) [![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayng/badge)](https://www.codefactor.io/repository/github/2dust/v2rayng) [![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayNG/latest/total?logo=github)](https://github.com/2dust/v2rayNG/releases) [![Chat on Telegram](https://img.shields.io/badge/Chat%20on-Telegram-brightgreen.svg)](https://t.me/v2rayn) + +Get it on Google Play + + ### Telegram Channel [github_2dust](https://t.me/github_2dust) @@ -17,7 +21,7 @@ A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-cor #### 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](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy) -- latest official [domain list](https://github.com/Loyalsoldier/v2ray-rules-dat) and [ip list](https://github.com/Loyalsoldier/geoip) can be imported manually +- latest official [domain list](https://github.com/v2fly/domain-list-community) and [ip list](https://github.com/v2fly/geoip) can be imported manually - possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6) ### More in our [wiki](https://github.com/2dust/v2rayNG/wiki) diff --git a/V2rayNG/.gitignore b/V2rayNG/.gitignore new file mode 100644 index 00000000..16eb6ec8 --- /dev/null +++ b/V2rayNG/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +*.apk +signing.properties +*.aar diff --git a/V2rayNG/app/.gitignore b/V2rayNG/app/.gitignore new file mode 100644 index 00000000..2abde4aa --- /dev/null +++ b/V2rayNG/app/.gitignore @@ -0,0 +1,2 @@ +/build +/google-services.json diff --git a/V2rayNG/app/build.gradle b/V2rayNG/app/build.gradle new file mode 100644 index 00000000..2519b3e8 --- /dev/null +++ b/V2rayNG/app/build.gradle @@ -0,0 +1,151 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +Properties props = new Properties() +props.load(new FileInputStream(new File('local.properties'))) + +android { + compileSdkVersion Integer.parseInt("$compileSdkVer") + buildToolsVersion "$buildToolsVer" + + compileOptions { + targetCompatibility = "8" + sourceCompatibility = "8" + } + + defaultConfig { + applicationId "com.v2ray.ang" + minSdkVersion 21 + targetSdkVersion Integer.parseInt("$targetSdkVer") + multiDexEnabled true + versionCode 488 + versionName "1.7.28" + } + + if (props["sign"]) { + signingConfigs { + release { + storeFile file("../key.jks") + keyAlias 'ang' + keyPassword '123456' + storePassword '123456' + } + debug { + storeFile file("../key.jks") + keyAlias 'ang' + keyPassword '123456' + storePassword '123456' + } + } + } + + buildTypes { + release { + minifyEnabled false + zipAlignEnabled false + shrinkResources false + if (props["sign"]) { + signingConfig signingConfigs.release + } + ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' +// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + minifyEnabled false + zipAlignEnabled false + shrinkResources false + if (props["sign"]) { + signingConfig signingConfigs.release + } + ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' + } + } + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + java.srcDirs += 'src/main/kotlin' + } + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } + + splits { + abi { + enable true + reset() + include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for + universalApk true //generate an additional APK that contains all the ABIs + } + } + + // map for the version code + project.ext.versionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4] + + android.applicationVariants.all { variant -> + // assign different version code for each output + variant.outputs.each { output -> + output.outputFileName = "v2rayNG_" + variant.versionName + "_" + output.getFilter(com.android.build.OutputFile.ABI) + ".apk" + + output.versionCodeOverride = + project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * + 1000000 + android.defaultConfig.versionCode + } + } + + buildFeatures { + viewBinding true + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: []) + testImplementation 'junit:junit:4.13.2' + + // Androidx + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.appcompat:appcompat:1.4.2' + implementation 'com.google.android.material:material:1.6.1' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.preference:preference-ktx:1.2.0' + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'androidx.fragment:fragment-ktx:1.5.2' + implementation 'androidx.multidex:multidex:2.0.1' + implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' + + // Androidx ktx + implementation 'androidx.activity:activity-ktx:1.5.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' + + //kotlin + implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2" + + implementation 'com.tencent:mmkv-static:1.2.12' + implementation 'com.google.code.gson:gson:2.8.9' + implementation 'io.reactivex:rxjava:1.3.4' + implementation 'io.reactivex:rxandroid:1.2.1' + implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar' + implementation 'me.dm7.barcodescanner:core:1.9.8' + implementation 'me.dm7.barcodescanner:zxing:1.9.8' + implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01@aar' + implementation 'me.drakeet.support:toastcompat:1.1.0' + implementation 'com.blacksquircle.ui:editorkit:2.1.1' + implementation 'com.blacksquircle.ui:language-base:2.1.1' + implementation 'com.blacksquircle.ui:language-json:2.1.1' +} + +//buildscript { +// repositories { +// google() +// mavenCentral() +// maven { url 'https://maven.google.com' } +// maven { url 'https://jitpack.io' } +// } +//} diff --git a/V2rayNG/app/build.gradle.kts b/V2rayNG/app/build.gradle.kts deleted file mode 100644 index 1624786c..00000000 --- a/V2rayNG/app/build.gradle.kts +++ /dev/null @@ -1,191 +0,0 @@ -plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - id("com.jaredsburrows.license") -} - -android { - namespace = "com.v2ray.ang" - compileSdk = 35 - - defaultConfig { - applicationId = "com.v2ray.ang" - minSdk = 21 - targetSdk = 35 - versionCode = 658 - versionName = "1.10.8" - multiDexEnabled = true - - val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';') - splits { - abi { - isEnable = 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" - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - flavorDimensions.add("distribution") - productFlavors { - create("fdroid") { - dimension = "distribution" - applicationIdSuffix = ".fdroid" - buildConfigField("String", "DISTRIBUTION", "\"F-Droid\"") - } - create("playstore") { - dimension = "distribution" - buildConfigField("String", "DISTRIBUTION", "\"Play Store\"") - } - } - - sourceSets { - getByName("main") { - jniLibs.srcDirs("libs") - } - } - - - compileOptions { - isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - - applicationVariants.all { - val variant = this - 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 = "v2rayNG_${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 = "v2rayNG_${variant.versionName}_${abi}.apk" - if (versionCodes.containsKey(abi)) { - output.versionCodeOverride = - (1000000 * versionCodes[abi]!!).plus(variant.versionCode) - } else { - return@forEach - } - } - } - } - - buildFeatures { - viewBinding = true - buildConfig = true - } - - packaging { - jniLibs { - useLegacyPackaging = true - } - } - -} - -dependencies { - // Core Libraries - implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar")))) - - // 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.androidx.swiperefreshlayout) - - // UI Libraries - implementation(libs.material) - implementation(libs.toasty) - implementation(libs.editorkit) - implementation(libs.flexbox) - - // Data and Storage Libraries - implementation(libs.mmkv.static) - implementation(libs.gson) - - // Reactive and Utility Libraries - implementation(libs.kotlinx.coroutines.android) - implementation(libs.kotlinx.coroutines.core) - - // Language and Processing Libraries - implementation(libs.language.base) - implementation(libs.language.json) - - // 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) -} diff --git a/V2rayNG/app/libs/arm64-v8a/libtun2socks.so b/V2rayNG/app/libs/arm64-v8a/libtun2socks.so new file mode 100644 index 00000000..3dbdde7a Binary files /dev/null and b/V2rayNG/app/libs/arm64-v8a/libtun2socks.so differ diff --git a/V2rayNG/app/libs/armeabi-v7a/libtun2socks.so b/V2rayNG/app/libs/armeabi-v7a/libtun2socks.so new file mode 100644 index 00000000..79e3a96a Binary files /dev/null and b/V2rayNG/app/libs/armeabi-v7a/libtun2socks.so differ diff --git a/V2rayNG/app/libs/libv2ray.aar b/V2rayNG/app/libs/libv2ray.aar new file mode 100644 index 00000000..238e1bc4 Binary files /dev/null and b/V2rayNG/app/libs/libv2ray.aar differ diff --git a/V2rayNG/app/libs/x86/libtun2socks.so b/V2rayNG/app/libs/x86/libtun2socks.so new file mode 100644 index 00000000..30a48fc2 Binary files /dev/null and b/V2rayNG/app/libs/x86/libtun2socks.so differ diff --git a/V2rayNG/app/libs/x86_64/libtun2socks.so b/V2rayNG/app/libs/x86_64/libtun2socks.so new file mode 100644 index 00000000..ce965f06 Binary files /dev/null and b/V2rayNG/app/libs/x86_64/libtun2socks.so differ diff --git a/V2rayNG/app/proguard-rules.pro b/V2rayNG/app/proguard-rules.pro index 481bb434..e69de29b 100644 --- a/V2rayNG/app/proguard-rules.pro +++ b/V2rayNG/app/proguard-rules.pro @@ -1,21 +0,0 @@ -# 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/androidTest/java/com/v2ray/ang/ApplicationTest.java b/V2rayNG/app/src/androidTest/java/com/v2ray/ang/ApplicationTest.java new file mode 100644 index 00000000..e221e714 --- /dev/null +++ b/V2rayNG/app/src/androidTest/java/com/v2ray/ang/ApplicationTest.java @@ -0,0 +1,13 @@ +package com.v2ray.ang; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/dev/res/values/strings.xml b/V2rayNG/app/src/dev/res/values/strings.xml deleted file mode 100644 index 697ffdc5..00000000 --- a/V2rayNG/app/src/dev/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - v2rayNG (DEV) - \ No newline at end of file diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml index 00e4b747..f90f1274 100644 --- a/V2rayNG/app/src/main/AndroidManifest.xml +++ b/V2rayNG/app/src/main/AndroidManifest.xml @@ -1,161 +1,119 @@ + package="com.v2ray.ang"> + android:normalScreens="true" + android:largeScreens="true" + android:xlargeScreens="true"/> - - - - - - + + - + + - - - - - + android:name=".ui.MainActivity" + android:launchMode="singleTask"> - - - + android:exported="false" + 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.RoutingSettingsActivity" + android:windowSoftInputMode="stateUnchanged" /> + 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="true" + android:name=".ui.UrlSchemeActivity"> - - - - - - - - + + + + - - @@ -165,107 +123,64 @@ - - + + + - - - + android:exported="true" + android:name=".receiver.WidgetProvider" + android:process=":RunSoLibV2RayDaemon"> + android:name="android.appwidget.provider" + android:resource="@xml/app_widget_provider" /> - - - - - + android:exported="true" + android:name=".service.QSTileService" + android:icon="@drawable/ic_stat_name" + android:label="@string/app_tile_name" + android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" + android:process=":RunSoLibV2RayDaemon"> - + android:name=".ui.TaskerActivity" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name"> + android:name=".receiver.TaskerReceiver" + android:process=":RunSoLibV2RayDaemon"> - - - - - - - - - diff --git a/V2rayNG/app/src/main/assets/custom_routing_black b/V2rayNG/app/src/main/assets/custom_routing_black deleted file mode 100644 index f15daacd..00000000 --- a/V2rayNG/app/src/main/assets/custom_routing_black +++ /dev/null @@ -1,142 +0,0 @@ -[ - { - "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": "绕过局域网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 new file mode 100644 index 00000000..a6ebf394 --- /dev/null +++ b/V2rayNG/app/src/main/assets/custom_routing_block @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000..5408992c --- /dev/null +++ b/V2rayNG/app/src/main/assets/custom_routing_direct @@ -0,0 +1,132 @@ +domain:12306.com, +domain:51ym.me, +domain:52pojie.cn, +domain:8686c.com, +domain:abercrombie.com, +domain:adobesc.com, +domain:air-matters.com, +domain:air-matters.io, +domain:airtable.com, +domain:akadns.net, +domain:apache.org, +domain:api.crisp.chat, +domain:api.termius.com, +domain:appshike.com, +domain:appstore.com, +domain:aweme.snssdk.com, +domain:bababian.com, +domain:battle.net, +domain:beatsbydre.com, +domain:bet365.com, +domain:bilibili.cn, +domain:ccgslb.com, +domain:ccgslb.net, +domain:chunbo.com, +domain:chunboimg.com, +domain:clashroyaleapp.com, +domain:cloudsigma.com, +domain:cloudxns.net, +domain:cmfu.com, +domain:culturedcode.com, +domain:dct-cloud.com, +domain:didialift.com, +domain:douyutv.com, +domain:duokan.com, +domain:dytt8.net, +domain:easou.com, +domain:ecitic.net, +domain:eclipse.org, +domain:eudic.net, +domain:ewqcxz.com, +domain:fir.im, +domain:frdic.com, +domain:fresh-ideas.cc, +domain:godic.net, +domain:goodread.com, +domain:haibian.com, +domain:hdslb.net, +domain:hollisterco.com, +domain:hongxiu.com, +domain:hxcdn.net, +domain:images.unsplash.com, +domain:img4me.com, +domain:ipify.org, +domain:ixdzs.com, +domain:jd.hk, +domain:jianshuapi.com, +domain:jomodns.com, +domain:jsboxbbs.com, +domain:knewone.com, +domain:kuaidi100.com, +domain:lemicp.com, +domain:letvcloud.com, +domain:lizhi.io, +domain:localizecdn.com, +domain:lucifr.com, +domain:luoo.net, +domain:mai.tn, +domain:maven.org, +domain:miwifi.com, +domain:moji.com, +domain:moke.com, +domain:mtalk.google.com, +domain:mxhichina.com, +domain:myqcloud.com, +domain:myunlu.com, +domain:netease.com, +domain:nfoservers.com, +domain:nssurge.com, +domain:nuomi.com, +domain:ourdvs.com, +domain:overcast.fm, +domain:paypal.com, +domain:paypalobjects.com, +domain:pgyer.com, +domain:qdaily.com, +domain:qdmm.com, +domain:qin.io, +domain:qingmang.me, +domain:qingmang.mobi, +domain:qqurl.com, +domain:rarbg.to, +domain:rrmj.tv, +domain:ruguoapp.com, +domain:sm.ms, +domain:snwx.com, +domain:soku.com, +domain:startssl.com, +domain:store.steampowered.com, +domain:symcd.com, +domain:teamviewer.com, +domain:tmzvps.com, +domain:trello.com, +domain:trellocdn.com, +domain:ttmeiju.com, +domain:udache.com, +domain:uxengine.net, +domain:weather.bjango.com, +domain:weather.com, +domain:webqxs.com, +domain:weico.cc, +domain:wenku8.net, +domain:werewolf.53site.com, +domain:windowsupdate.com, +domain:wkcdn.com, +domain:workflowy.com, +domain:xdrig.com, +domain:xiaojukeji.com, +domain:xiaomi.net, +domain:xiaomicp.com, +domain:ximalaya.com, +domain:xitek.com, +domain:xmcdn.com, +domain:xslb.net, +domain:xteko.com, +domain:yach.me, +domain:yixia.com, +domain:yunjiasu-cdn.net, +domain:zealer.com, +domain:zgslb.net, +domain:zimuzu.tv, +domain:zmz002.com, +domain:samsungdm.com, \ 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 deleted file mode 100644 index 21bfb24d..00000000 --- a/V2rayNG/app/src/main/assets/custom_routing_global +++ /dev/null @@ -1,27 +0,0 @@ -[ - { - "remarks": "阻断udp443", - "outboundTag": "block", - "port": "443", - "network": "udp" - }, - { - "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_proxy b/V2rayNG/app/src/main/assets/custom_routing_proxy new file mode 100644 index 00000000..d67a6237 --- /dev/null +++ b/V2rayNG/app/src/main/assets/custom_routing_proxy @@ -0,0 +1,33 @@ +geosite:google, +geosite:github, +geosite:netflix, +geosite:steam, +geosite:telegram, +geosite:tumblr, +geosite:speedtest, +geosite:bbc, +domain:gvt1.com, +domain:textnow.com, +domain:twitch.tv, +domain:wikileaks.org, +domain:naver.com, +91.108.4.0/22, +91.108.8.0/22, +91.108.12.0/22, +91.108.20.0/22, +91.108.36.0/23, +91.108.38.0/23, +91.108.56.0/22, +149.154.160.0/20, +149.154.164.0/22, +149.154.172.0/22, +74.125.0.0/16, +173.194.0.0/16, +172.217.0.0/16, +216.58.200.0/24, +216.58.220.0/24, +91.108.56.116, +91.108.56.0/24, +109.239.140.0/24, +149.154.167.0/24, +149.154.175.0/24, \ 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 deleted file mode 100644 index e3c360c0..00000000 --- a/V2rayNG/app/src/main/assets/custom_routing_white +++ /dev/null @@ -1,96 +0,0 @@ -[ - { - "remarks": "Google cn", - "outboundTag": "proxy", - "domain": [ - "domain:googleapis.cn", - "domain:gstatic.com" - ] - }, - { - "remarks": "阻断udp443", - "outboundTag": "block", - "port": "443", - "network": "udp" - }, - { - "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 deleted file mode 100644 index 97ff1227..00000000 --- a/V2rayNG/app/src/main/assets/custom_routing_white_iran +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "remarks": "Block udp443", - "outboundTag": "block", - "port": "443", - "network": "udp" - }, - { - "remarks": "Direct LAN IP", - "outboundTag": "direct", - "ip": [ - "geoip:private" - ] - }, - { - "remarks": "Direct LAN domains", - "outboundTag": "direct", - "domain": [ - "geosite:private" - ] - }, - { - "remarks": "Bypass 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 deleted file mode 100644 index 0cfd1daa..00000000 --- a/V2rayNG/app/src/main/assets/open_source_licenses.html +++ /dev/null @@ -1,1285 +0,0 @@ - - - - - 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/assets/proxy_packagename.txt b/V2rayNG/app/src/main/assets/proxy_packagename.txt index 12abf7db..ecac8251 100644 --- a/V2rayNG/app/src/main/assets/proxy_packagename.txt +++ b/V2rayNG/app/src/main/assets/proxy_packagename.txt @@ -4,7 +4,6 @@ au.com.shiftyjelly.pocketcasts bbc.mobile.news.ww be.mygod.vpnhotspot ch.protonmail.android -cm.aptoide.pt co.wanqu.android com.alphainventor.filemanager com.amazon.kindle @@ -35,9 +34,7 @@ com.chrome.canary com.chrome.dev com.cl.newt66y com.cradle.iitc_mobile -org.exarhteam.iitc_mobile com.cygames.shadowverse -com.dcard.freedom com.devhd.feedly com.devolver.reigns2 com.discord @@ -111,7 +108,6 @@ com.ifttt.ifttt com.imgur.mobile com.innologica.inoreader com.instagram.android -com.instagram.lite com.instapaper.android com.jarvanh.vpntether com.kapp.youtube.final @@ -119,7 +115,6 @@ com.klinker.android.twitter_l com.lastpass.lpandroid com.linecorp.linelite com.lingodeer -com.ltnnews.news com.mediapods.tumbpods com.mgoogle.android.gms com.microsoft.emmx @@ -164,7 +159,6 @@ com.slack com.snaptube.premium com.sololearn com.sonelli.juicessh -com.sparkslab.dcardreader com.spotify.music com.tencent.huatuo com.termux @@ -179,13 +173,10 @@ com.twitter.android com.u91porn com.u9porn com.ubisoft.dance.justdance2015companion -com.udn.news com.utopia.pxview +com.valvesoftware.android.steam.communimunity com.valvesoftware.android.steam.community -com.vanced.manager com.vanced.android.youtube -com.vanced.android.apps.youtube.music -com.mgoogle.android.gms com.vimeo.android.videoapp com.vivaldi.browser com.vivaldi.browser.snapshot @@ -195,12 +186,10 @@ com.wire com.wuxiangai.refactor com.xda.labs com.xvideos.app -com.yahoo.mobile.client.android.superapp com.yandex.browser com.yandex.browser.beta com.yandex.browser.alpha com.z28j.feel -com.zhiliaoapp.musically con.medium.reader de.apkgrabber de.robv.android.xposed.installer @@ -221,7 +210,6 @@ jp.bokete.app.android jp.naver.line.android jp.pxv.android luo.speedometergpspro -m.cna.com.tw.App mark.via.gp me.tshine.easymark net.teeha.android.url_shortener @@ -238,7 +226,6 @@ org.mozilla.firefox_beta org.mozilla.focus org.schabi.newpipe org.telegram.messenger -org.telegram.messenger.web org.telegram.multi org.telegram.plus org.thunderdog.challegram @@ -252,162 +239,3 @@ tw.com.gamer.android.activecenter videodownloader.downloadvideo.downloader uk.co.bbc.learningenglish com.ted.android -de.danoeh.antennapod -com.kiwibrowser.browser -nekox.messenger -com.nextcloud.client -com.aurora.store -com.aurora.adroid -chat.simplex.app -im.vector.app -network.loki.messenger -eu.siacs.conversations -xyz.nextalone.nagram -net.programmierecke.radiodroid2 -im.fdx.v2ex -ml.docilealligator.infinityforreddit -com.bytemyth.ama -app.vanadium.browser -com.cakewallet.cake_wallet -org.purplei2p.i2pd -dk.tacit.android.foldersync.lite -com.nononsenseapps.feeder -com.m2049r.xmrwallet -com.paypal.android.p2pmobile -com.google.android.apps.googlevoice -com.readdle.spark -org.torproject.torbrowser -com.deepl.mobiletranslator -com.microsoft.bing -com.keylesspalace.tusky -com.ottplay.ottplay -ru.iptvremote.android.iptv.pro -jp.naver.line.android -com.xmflsct.app.tooot -com.forem.android -app.revanced.android.youtube -com.mgoogle.android.gms -com.pionex.client -vip.mytokenpocket -im.token.app -com.linekong.mars24 -com.feixiaohao -com.aicoin.appandroid -com.binance.dev -com.kraken.trade -com.okinc.okex.gp -com.authy.authy -air.com.rosettastone.mobile.CoursePlayer -com.blizzard.bma -com.amazon.kindle -com.google.android.apps.fitness -net.tsapps.appsales -com.wemesh.android -com.google.android.apps.googleassistant -allen.town.focus.reader -me.hyliu.fluent_reader_lite -com.aljazeera.mobile -com.ft.news -de.marmaro.krt.ffupdater -myradio.radio.fmradio.liveradio.radiostation -com.google.earth -eu.kanade.tachiyomi.j2k -com.audials -com.microsoft.skydrive -com.mb.android.tg -com.melodis.midomiMusicIdentifier.freemium -com.foxnews.android -ch.threema.app -com.briarproject.briar.android -foundation.e.apps -com.valvesoftware.android.steam.friendsui -com.imback.yeetalk -so.onekey.app.wallet -com.xc3fff0e.xmanager -meditofoundation.medito -com.picol.client -com.streetwriters.notesnook -shanghai.panewsApp.com -org.coursera.android -com.positron_it.zlib -com.blizzard.messenger -com.javdb.javrocket -com.picacomic.fregata -com.fxl.chacha -me.proton.android.drive -com.lastpass.lpandroid -com.tradingview.tradingviewapp -com.deviantart.android.damobile -com.fusionmedia.investing -com.ewa.ewaapp -com.duolingo -com.hellotalk -io.github.huskydg.magisk -com.jsy.xpgbox -com.hostloc.app.hostloc -com.dena.pokota -com.vitorpamplona.amethyst -com.zhiliaoapp.musically -us.spotco.fennec_dos -com.fongmi.android.tv -com.pocketprep.android.itcybersecurity -com.cloudtv -com.glassdoor.app -com.indeed.android.jobsearch -com.linkedin.android -com.github.tvbox.osc.bh -com.example.douban -com.sipnetic.app -com.microsoft.rdc.androidx -org.zwanoo.android.speedtest -com.sonelli.juicessh -com.scmp.newspulse -org.lsposed.manager -mnn.Android -com.thomsonretuers.reuters -com.guardian -com.ttxapps.onesyncv2 -org.fcitx.fcitx5.android.updater -com.tailscale.ipn -tw.nekomimi.nekogram -com.nexon.kartdrift -io.syncapps.lemmy_sync -com.seazon.feedme -com.readwise -de.spiritcroc.riotx -com.openai.chatgpt -io.changenow.changenow -com.poe.android -com.twingate -com.blinkslabs.blinkist.android -com.ichi2.anki -md.obsidian -com.musixmatch.android.lyrify -com.cyber.turbo -com.offsec.nethunter -me.ghui.v2er -com.samruston.twitter -org.adaway -org.swiftapps.swiftbackup -com.zerotier.one -com.quietmobile -com.instagram.barcelona -im.molly.app -com.rvx.android.youtube -com.deepl.mobiletranslator -com.qingsong.yingmi -com.lemurbrowser.exts -com.silverdev.dnartdroid -me.ash.reader -de.tutao.tutanota -dev.imranr.obtainium -com.getsomeheadspace.android -org.cromite.cromite -com.nutomic.syncthingandroid -com.bumble.app -com.cnn.mobile.android.phone -com.google.android.apps.authenticator2 -com.microsoft.copilot -com.netflix.NGP.Storyteller -com.Slack -com.server.auditor.ssh.client \ No newline at end of file diff --git a/V2rayNG/app/src/main/assets/v2ray_config.json b/V2rayNG/app/src/main/assets/v2ray_config.json index 4f8c3d7e..57b1bd8a 100644 --- a/V2rayNG/app/src/main/assets/v2ray_config.json +++ b/V2rayNG/app/src/main/assets/v2ray_config.json @@ -81,9 +81,7 @@ }, { "protocol": "freedom", - "settings": { - "domainStrategy": "UseIP" - }, + "settings": {}, "tag": "direct" }, { @@ -97,7 +95,7 @@ } ], "routing": { - "domainStrategy": "AsIs", + "domainStrategy": "IPIfNonMatch", "rules": [] }, "dns": { diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt deleted file mode 100644 index 44f680b3..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.v2ray.ang - -import android.content.Context -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 - -class AngApplication : MultiDexApplication() { - companion object { - lateinit var application: AngApplication - } - - /** - * Attaches the base context to the application. - * @param base The base context. - */ - override fun attachBaseContext(base: Context?) { - super.attachBaseContext(base) - application = this - } - - private val workManagerConfiguration: Configuration = Configuration.Builder() - .setDefaultProcessName("${ANG_PACKAGE}:bg") - .build() - - /** - * Initializes the application. - */ - override fun onCreate() { - super.onCreate() - - MMKV.initialize(this) - - SettingsManager.setNightMode() - // Initialize WorkManager with the custom configuration - WorkManager.initialize(this, workManagerConfiguration) - - SettingsManager.initRoutingRulesets(this) - - es.dmoral.toasty.Toasty.Config.getInstance() - .setGravity(android.view.Gravity.BOTTOM, 0, 200) - .apply() - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt deleted file mode 100644 index 09e3a9d5..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt +++ /dev/null @@ -1,248 +0,0 @@ -package com.v2ray.ang - - -object AppConfig { - - /** The application's package name. */ - const val ANG_PACKAGE = BuildConfig.APPLICATION_ID - const val TAG = BuildConfig.APPLICATION_ID - - /** Directory names used in the app's file system. */ - const val DIR_ASSETS = "assets" - const val DIR_BACKUPS = "backups" - - /** Legacy configuration keys. */ - const val ANG_CONFIG = "ang_config" - - /** Preferences mapped to MMKV storage. */ - const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled" - const val PREF_ROUTE_ONLY_ENABLED = "pref_route_only_enabled" - const val PREF_PER_APP_PROXY = "pref_per_app_proxy" - const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set" - 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_VPN_INTERFACE_ADDRESS_CONFIG_INDEX = "pref_vpn_interface_address_config_index" - const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy" - 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" - const val PREF_MUX_XUDP_QUIC = "pref_mux_xudp_quic" - const val PREF_FRAGMENT_ENABLED = "pref_fragment_enabled" - const val PREF_FRAGMENT_PACKETS = "pref_fragment_packets" - const val PREF_FRAGMENT_LENGTH = "pref_fragment_length" - const val PREF_FRAGMENT_INTERVAL = "pref_fragment_interval" - const val SUBSCRIPTION_AUTO_UPDATE = "pref_auto_update_subscription" - const val SUBSCRIPTION_AUTO_UPDATE_INTERVAL = "pref_auto_update_interval" - const val SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL = "1440" // Default is 24 hours - const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater" - const val PREF_SPEED_ENABLED = "pref_speed_enabled" - const val PREF_CONFIRM_REMOVE = "pref_confirm_remove" - const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate" - const val PREF_DOUBLE_COLUMN_DISPLAY = "pref_double_column_display" - const val PREF_LANGUAGE = "pref_language" - const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night" - const val PREF_PREFER_IPV6 = "pref_prefer_ipv6" - 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_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_OUTBOUND_DOMAIN_RESOLVE_METHOD = "pref_outbound_domain_resolve_method" - const val PREF_MODE = "pref_mode" - const val PREF_IS_BOOTED = "pref_is_booted" - const val PREF_CHECK_UPDATE_PRE_RELEASE = "pref_check_update_pre_release" - const val PREF_GEO_FILES_SOURCES = "pref_geo_files_sources" - - /** Cache keys. */ - const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id" - const val CACHE_KEYWORD_FILTER = "cache_keyword_filter" - - /** Protocol identifiers. */ - const val PROTOCOL_FREEDOM = "freedom" - - /** Broadcast actions. */ - const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service" - const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity" - const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click" - - /** Tasker extras. */ - const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE" - const val TASKER_EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB" - const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch" - const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid" - const val TASKER_DEFAULT_GUID = "Default" - - /** Tags for different proxy modes. */ - const val TAG_PROXY = "proxy" - const val TAG_DIRECT = "direct" - const val TAG_BLOCKED = "block" - const val TAG_FRAGMENT = "fragment" - - /** Network-related constants. */ - const val UPLINK = "uplink" - const val DOWNLINK = "downlink" - - /** URLs for various resources. */ - const val GITHUB_URL = "https://github.com" - const val GITHUB_RAW_URL = "https://raw.githubusercontent.com" - const val GITHUB_DOWNLOAD_URL = "$GITHUB_URL/%s/releases/latest/download" - const val ANDROID_PACKAGE_NAME_LIST_URL = "$GITHUB_RAW_URL/2dust/androidpackagenamelist/master/proxy.txt" - const val APP_URL = "$GITHUB_URL/2dust/v2rayNG" - const val APP_API_URL = "https://api.github.com/repos/2dust/v2rayNG/releases" - const val APP_ISSUES_URL = "$APP_URL/issues" - const val APP_WIKI_MODE = "$APP_URL/wiki/Mode" - const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md" - const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw=" - const val TG_CHANNEL_URL = "https://t.me/github_2dust" - const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204" - const val DELAY_TEST_URL2 = "https://www.google.com/generate_204" - const val IP_API_URL = "https://speed.cloudflare.com/meta" - - /** DNS server addresses. */ - const val DNS_PROXY = "1.1.1.1" - const val DNS_DIRECT = "223.5.5.5" - 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 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 LOOPBACK = "127.0.0.1" - - /** Message constants for communication. */ - const val MSG_REGISTER_CLIENT = 1 - const val MSG_STATE_RUNNING = 11 - const val MSG_STATE_NOT_RUNNING = 12 - const val MSG_UNREGISTER_CLIENT = 2 - const val MSG_STATE_START = 3 - const val MSG_STATE_START_SUCCESS = 31 - const val MSG_STATE_START_FAILURE = 32 - const val MSG_STATE_STOP = 4 - const val MSG_STATE_STOP_SUCCESS = 41 - const val MSG_STATE_RESTART = 5 - const val MSG_MEASURE_DELAY = 6 - const val MSG_MEASURE_DELAY_SUCCESS = 61 - const val MSG_MEASURE_CONFIG = 7 - const val MSG_MEASURE_CONFIG_SUCCESS = 71 - const val MSG_MEASURE_CONFIG_CANCEL = 72 - - /** 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 SUBSCRIPTION_UPDATE_CHANNEL = "subscription_update_channel" - const val SUBSCRIPTION_UPDATE_CHANNEL_NAME = "Subscription Update Service" - - /** Protocols Scheme **/ - const val VMESS = "vmess://" - 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_ONE_DOMAIN = "one.one.one.one" - const val DNS_CLOUDFLARE_DNS_COM_DOMAIN = "dns.cloudflare.com" - const val DNS_CLOUDFLARE_DNS_DOMAIN = "cloudflare-dns.com" - 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" - - 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" - - val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1") - val DNS_CLOUDFLARE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001") - val DNS_CLOUDFLARE_DNS_COM_ADDRESSES = arrayListOf("104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5") - val DNS_CLOUDFLARE_DNS_ADDRESSES = arrayListOf("104.16.248.249", "104.16.249.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9") - 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") - - //minimum list https://serverfault.com/a/304791 - val ROUTED_IP_LIST = arrayListOf( - "0.0.0.0/5", - "8.0.0.0/7", - "11.0.0.0/8", - "12.0.0.0/6", - "16.0.0.0/4", - "32.0.0.0/3", - "64.0.0.0/2", - "128.0.0.0/3", - "160.0.0.0/5", - "168.0.0.0/6", - "172.0.0.0/12", - "172.32.0.0/11", - "172.64.0.0/10", - "172.128.0.0/9", - "173.0.0.0/8", - "174.0.0.0/7", - "176.0.0.0/4", - "192.0.0.0/9", - "192.128.0.0/11", - "192.160.0.0/13", - "192.169.0.0/16", - "192.170.0.0/15", - "192.172.0.0/14", - "192.176.0.0/12", - "192.192.0.0/10", - "193.0.0.0/8", - "194.0.0.0/7", - "196.0.0.0/6", - "200.0.0.0/5", - "208.0.0.0/4", - "240.0.0.0/4" - ) - - val PRIVATE_IP_LIST = arrayListOf( - "0.0.0.0/8", - "10.0.0.0/8", - "127.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "169.254.0.0/16", - "224.0.0.0/4" - ) - - val GEO_FILES_SOURCES = arrayListOf( - "Loyalsoldier/v2ray-rules-dat", - "runetfreedom/russia-v2ray-rules-dat", - "Chocolate4U/Iran-v2ray-rules" - ) - -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt deleted file mode 100644 index 219e35e9..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.v2ray.ang.dto - -import android.graphics.drawable.Drawable - -data class AppInfo( - val appName: String, - val packageName: String, - val appIcon: Drawable, - val isSystemApp: Boolean, - var isSelected: Int -) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt deleted file mode 100644 index 5a8d1e60..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.v2ray.ang.dto - -data class AssetUrlItem( - var remarks: String = "", - var url: String = "", - val addedTime: Long = System.currentTimeMillis(), - var lastUpdated: Long = -1, - var locked: Boolean? = false, -) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt deleted file mode 100644 index be4f62e5..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.v2ray.ang.dto - -data class CheckUpdateResult( - val hasUpdate: Boolean, - val latestVersion: String? = null, - val releaseNotes: String? = null, - val downloadUrl: String? = null, - val error: String? = null, - val isPreRelease: Boolean = false -) \ No newline at end of file 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 deleted file mode 100644 index c8870248..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.v2ray.ang.dto - -data class ConfigResult( - var status: Boolean, - var guid: String? = null, - var content: String = "", - var socksPort: Int? = null, -) - diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/EConfigType.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/EConfigType.kt deleted file mode 100644 index ed7cad3b..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/EConfigType.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.v2ray.ang.dto - -import com.v2ray.ang.AppConfig - - -enum class EConfigType(val value: Int, val protocolScheme: String) { - VMESS(1, AppConfig.VMESS), - CUSTOM(2, AppConfig.CUSTOM), - SHADOWSOCKS(3, AppConfig.SHADOWSOCKS), - SOCKS(4, AppConfig.SOCKS), - VLESS(5, AppConfig.VLESS), - TROJAN(6, AppConfig.TROJAN), - WIREGUARD(7, AppConfig.WIREGUARD), - - // TUIC(8, AppConfig.TUIC), - HYSTERIA2(9, AppConfig.HYSTERIA2), - HTTP(10, AppConfig.HTTP); - - companion object { - fun fromInt(value: Int) = entries.firstOrNull { it.value == value } - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt deleted file mode 100644 index 0a7dce56..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.v2ray.ang.dto - -import com.google.gson.annotations.SerializedName - -data class GitHubRelease( - @SerializedName("tag_name") - val tagName: String, - @SerializedName("body") - val body: String, - @SerializedName("assets") - val assets: List, - @SerializedName("prerelease") - val prerelease: Boolean = false, - @SerializedName("published_at") - val publishedAt: String = "" -) { - data class Asset( - @SerializedName("name") - val name: String, - @SerializedName("browser_download_url") - val browserDownloadUrl: String - ) -} \ No newline at end of file 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 deleted file mode 100644 index c4fc6582..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Hysteria2Bean.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.v2ray.ang.dto - -data class Hysteria2Bean( - val server: String?, - val auth: String?, - val lazy: Boolean? = true, - val obfs: ObfsBean? = null, - val socks5: Socks5Bean? = null, - val http: Socks5Bean? = null, - val tls: TlsBean? = null, - val transport: TransportBean? = null, - val bandwidth: BandwidthBean? = null, -) { - data class ObfsBean( - val type: String?, - val salamander: SalamanderBean? - ) { - data class SalamanderBean( - val password: String?, - ) - } - - data class Socks5Bean( - val listen: String?, - ) - - data class TlsBean( - val sni: String?, - val insecure: Boolean?, - val pinSHA256: String?, - ) - - data class TransportBean( - val type: String?, - val udp: TransportUdpBean? - ) { - data class TransportUdpBean( - val hopInterval: String?, - ) - } - - data class BandwidthBean( - val down: String?, - val up: String?, - ) -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt deleted file mode 100644 index 97814fbb..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.v2ray.ang.dto - -data class IPAPIInfo( - var ip: String? = null, - var clientIp: String? = null, - var ip_addr: String? = null, - var query: String? = null, - var country: String? = null, - var country_name: String? = null, - var country_code: String? = null, - var countryCode: String? = null -) \ 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 deleted file mode 100644 index 46333d06..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Language.kt +++ /dev/null @@ -1,20 +0,0 @@ -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"), - ARABIC("ar"), - 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 deleted file mode 100644 index 0e8091b9..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 7a1f7346..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileItem.kt +++ /dev/null @@ -1,120 +0,0 @@ -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 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.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/java/com/v2ray/ang/dto/RoutingType.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RoutingType.kt deleted file mode 100644 index a98ac167..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RoutingType.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.v2ray.ang.dto - -enum class RoutingType(val fileName: String) { - WHITE("custom_routing_white"), - BLACK("custom_routing_black"), - GLOBAL("custom_routing_global"), - WHITE_IRAN("custom_routing_white_iran"); - - companion object { - fun fromIndex(index: Int): RoutingType { - return when (index) { - 0 -> WHITE - 1 -> BLACK - 2 -> GLOBAL - 3 -> WHITE_IRAN - 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 deleted file mode 100644 index 9005f559..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RulesetItem.kt +++ /dev/null @@ -1,13 +0,0 @@ -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/java/com/v2ray/ang/dto/ServerConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerConfig.kt deleted file mode 100644 index baa1fe48..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerConfig.kt +++ /dev/null @@ -1,86 +0,0 @@ -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 - -data class ServerConfig( - val configVersion: Int = 3, - val configType: EConfigType, - var subscriptionId: String = "", - val addedTime: Long = System.currentTimeMillis(), - var remarks: String = "", - val outboundBean: V2rayConfig.OutboundBean? = null, - var fullConfig: V2rayConfig? = null -) { - companion object { - fun create(configType: EConfigType): ServerConfig { - when (configType) { - EConfigType.VMESS, - EConfigType.VLESS -> - return ServerConfig( - configType = configType, - outboundBean = V2rayConfig.OutboundBean( - protocol = configType.name.lowercase(), - settings = V2rayConfig.OutboundBean.OutSettingsBean( - vnext = listOf( - V2rayConfig.OutboundBean.OutSettingsBean.VnextBean( - users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean()) - ) - ) - ), - streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean() - ) - ) - - EConfigType.CUSTOM -> - return ServerConfig(configType = configType) - - EConfigType.SHADOWSOCKS, - EConfigType.SOCKS, - EConfigType.HTTP, - EConfigType.TROJAN, - EConfigType.HYSTERIA2 -> - return ServerConfig( - configType = configType, - outboundBean = V2rayConfig.OutboundBean( - protocol = configType.name.lowercase(), - settings = V2rayConfig.OutboundBean.OutSettingsBean( - servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean()) - ), - streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean() - ) - ) - - EConfigType.WIREGUARD -> - return ServerConfig( - configType = configType, - outboundBean = V2rayConfig.OutboundBean( - protocol = configType.name.lowercase(), - settings = V2rayConfig.OutboundBean.OutSettingsBean( - secretKey = "", - peers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean()) - ) - ) - ) - } - } - } - - fun getProxyOutbound(): V2rayConfig.OutboundBean? { - if (configType != EConfigType.CUSTOM) { - return outboundBean - } - return fullConfig?.getProxyOutbound() - } - - fun getAllOutboundTags(): MutableList { - if (configType != EConfigType.CUSTOM) { - return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED) - } - fullConfig?.let { config -> - return config.outbounds.map { it.tag }.toMutableList() - } - return mutableListOf() - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServersCache.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServersCache.kt deleted file mode 100644 index 7ea3e6a3..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServersCache.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.v2ray.ang.dto - -data class ServersCache( - val guid: String, - val profile: ProfileItem -) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt deleted file mode 100644 index 8957df78..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.v2ray.ang.dto - -data class SubscriptionItem( - var remarks: String = "", - var url: String = "", - var enabled: Boolean = true, - val addedTime: Long = System.currentTimeMillis(), - var lastUpdated: Long = -1, - var autoUpdate: Boolean = false, - val updateInterval: Int? = null, - var prevProfile: String? = null, - var nextProfile: String? = null, - var filter: String? = null, - var allowInsecureUrl: Boolean = false, -) - diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt deleted file mode 100644 index 155be104..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt +++ /dev/null @@ -1,556 +0,0 @@ -package com.v2ray.ang.dto - -import com.google.gson.annotations.SerializedName -import com.v2ray.ang.AppConfig -import com.v2ray.ang.util.Utils - -data class V2rayConfig( - var remarks: String? = null, - var stats: Any? = null, - val log: LogBean, - var policy: PolicyBean? = null, - val inbounds: ArrayList, - var outbounds: ArrayList, - var dns: DnsBean? = null, - val routing: RoutingBean, - val api: Any? = null, - val transport: Any? = null, - val reverse: Any? = null, - var fakedns: Any? = null, - val browserForwarder: Any? = null, - var observatory: Any? = null, - var burstObservatory: Any? = null -) { - - data class LogBean( - val access: String? = null, - val error: String? = null, - var loglevel: String? = null, - val dnsLog: Boolean? = null - ) - - data class InboundBean( - var tag: String, - var port: Int, - var protocol: String, - var listen: String? = null, - val settings: Any? = null, - val sniffing: SniffingBean? = null, - val streamSettings: Any? = null, - val allocate: Any? = null - ) { - - data class InSettingsBean( - val auth: String? = null, - val udp: Boolean? = null, - val userLevel: Int? = null, - val address: String? = null, - val port: Int? = null, - val network: String? = null - ) - - data class SniffingBean( - var enabled: Boolean, - val destOverride: ArrayList, - val metadataOnly: Boolean? = null, - var routeOnly: Boolean? = null - ) - } - - data class OutboundBean( - var tag: String = "proxy", - var protocol: String, - var settings: OutSettingsBean? = null, - var streamSettings: StreamSettingsBean? = null, - val proxySettings: Any? = null, - val sendThrough: String? = null, - var mux: MuxBean? = MuxBean(false) - ) { - 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, - ) { - - 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? = null, - var level: Int = AppConfig.DEFAULT_LEVEL, - var encryption: String? = null, - var flow: String? = null - ) - } - - data class FragmentBean( - var packets: String? = null, - var length: String? = null, - 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? = null, - var ota: Boolean = false, - 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 = AppConfig.DEFAULT_LEVEL - ) - } - - data class Response(var type: String) - - data class WireGuardBean( - var publicKey: String = "", - var preSharedKey: String? = null, - var endpoint: String = "" - ) - } - - data class StreamSettingsBean( - 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 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 - ) { - - data class TcpSettingsBean( - var header: HeaderBean = HeaderBean(), - val acceptProxyProtocol: Boolean? = null - ) { - data class HeaderBean( - var type: String = "none", - var request: RequestBean? = null, - var response: Any? = null - ) { - data class RequestBean( - var path: List = ArrayList(), - var headers: HeadersBean = HeadersBean(), - val version: String? = null, - val method: String? = null - ) { - data class HeadersBean( - var Host: List? = ArrayList(), - @SerializedName("User-Agent") - val userAgent: List? = null, - @SerializedName("Accept-Encoding") - val acceptEncoding: List? = null, - val Connection: List? = null, - val Pragma: String? = null - ) - } - } - } - - data class KcpSettingsBean( - var mtu: Int = 1350, - var tti: Int = 50, - var uplinkCapacity: Int = 12, - var downlinkCapacity: Int = 100, - var congestion: Boolean = false, - var readBufferSize: Int = 1, - var writeBufferSize: Int = 1, - var header: HeaderBean = HeaderBean(), - var seed: String? = null - ) { - data class HeaderBean( - var type: String = "none", - var domain: String? = null - ) - } - - data class WsSettingsBean( - var path: String? = null, - var headers: HeadersBean = HeadersBean(), - val maxEarlyData: Int? = null, - val useBrowserForwarding: Boolean? = null, - val acceptProxyProtocol: Boolean? = null - ) { - data class HeadersBean(var Host: String = "") - } - - data class HttpupgradeSettingsBean( - var path: String? = null, - var host: String? = null, - val acceptProxyProtocol: Boolean? = 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? = null - ) - - data class SockoptBean( - var TcpNoDelay: Boolean? = null, - var tcpKeepAliveIdle: Int? = null, - var tcpFastOpen: Boolean? = null, - var tproxy: String? = null, - var mark: Int? = null, - var dialerProxy: String? = null, - var domainStrategy: String? = null - ) - - data class TlsSettingsBean( - var allowInsecure: Boolean = false, - var serverName: String? = null, - val alpn: List? = null, - val minVersion: String? = null, - val maxVersion: String? = null, - val preferServerCipherSuites: Boolean? = null, - val cipherSuites: String? = null, - val fingerprint: String? = null, - val certificates: List? = null, - val disableSystemRoot: Boolean? = null, - val enableSessionResumption: Boolean? = null, - // REALITY settings - val show: Boolean = false, - var publicKey: String? = null, - var shortId: String? = null, - var spiderX: String? = null - ) - - data class QuicSettingBean( - var security: String = "none", - var key: String = "", - var header: HeaderBean = HeaderBean() - ) { - data class HeaderBean(var type: String = "none") - } - - data class GrpcSettingsBean( - var serviceName: String = "", - var authority: String? = null, - var multiMode: Boolean? = null, - var idle_timeout: Int? = null, - 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, - ) - } - - } - - data class MuxBean( - var enabled: Boolean, - 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?.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?.first()?.address - } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) { - return settings?.peers?.first()?.endpoint?.substringBeforeLast(":") - } - return null - } - - fun getServerPort(): Int? { - if (protocol.equals(EConfigType.VMESS.name, true) - || protocol.equals(EConfigType.VLESS.name, true) - ) { - 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?.first()?.port - } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) { - 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?.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?.first()?.users?.first()?.pass - } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) { - return settings?.secretKey - } - return null - } - - fun getSecurityEncryption(): String? { - return when { - protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.first()?.users?.first()?.security - protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.first()?.users?.first()?.encryption - protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.first()?.method - else -> null - } - } - - fun getTransportSettingDetails(): List? { - if (protocol.equals(EConfigType.VMESS.name, true) - || protocol.equals(EConfigType.VLESS.name, true) - || protocol.equals(EConfigType.TROJAN.name, true) - || protocol.equals(EConfigType.SHADOWSOCKS.name, true) - ) { - val transport = streamSettings?.network ?: return null - return when (transport) { - NetworkType.TCP.type -> { - val tcpSetting = streamSettings?.tcpSettings ?: return null - listOf( - tcpSetting.header.type, - tcpSetting.header.request?.headers?.Host?.joinToString(",").orEmpty(), - tcpSetting.header.request?.path?.joinToString(",").orEmpty() - ) - } - - NetworkType.KCP.type -> { - val kcpSetting = streamSettings?.kcpSettings ?: return null - listOf( - kcpSetting.header.type, - "", - kcpSetting.seed.orEmpty() - ) - } - - NetworkType.WS.type -> { - val wsSetting = streamSettings?.wsSettings ?: return null - listOf( - "", - wsSetting.headers.Host, - wsSetting.path - ) - } - - NetworkType.HTTP_UPGRADE.type -> { - val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null - listOf( - "", - httpupgradeSetting.host, - httpupgradeSetting.path - ) - } - - NetworkType.XHTTP.type -> { - val xhttpSettings = streamSettings?.xhttpSettings ?: return null - listOf( - "", - xhttpSettings.host, - xhttpSettings.path - ) - } - - NetworkType.H2.type -> { - val h2Setting = streamSettings?.httpSettings ?: return null - listOf( - "", - h2Setting.host.joinToString(","), - h2Setting.path - ) - } - -// "quic" -> { -// val quicSetting = streamSettings?.quicSettings ?: return null -// listOf( -// quicSetting.header.type, -// quicSetting.security, -// quicSetting.key -// ) -// } - - NetworkType.GRPC.type -> { - val grpcSetting = streamSettings?.grpcSettings ?: return null - listOf( - if (grpcSetting.multiMode == true) "multi" else "gun", - grpcSetting.authority.orEmpty(), - grpcSetting.serviceName - ) - } - - else -> null - } - } - return null - } - - fun ensureSockopt(): V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean { - val stream = streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean().also { - streamSettings = it - } - - val sockopt = stream.sockopt ?: V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean().also { - stream.sockopt = it - } - - return sockopt - } - } - - data class DnsBean( - var servers: ArrayList? = null, - var hosts: Map? = null, - val clientIp: String? = null, - val disableCache: Boolean? = null, - val queryStrategy: String? = null, - val tag: String? = null - ) { - data class ServersBean( - var address: String = "", - var port: Int? = null, - var domains: List? = null, - var expectIPs: List? = null, - val clientIp: String? = null, - val skipFallback: Boolean? = null, - ) - } - - data class RoutingBean( - var domainStrategy: String, - var domainMatcher: String? = null, - var rules: ArrayList, - val balancers: List? = null - ) { - - data class RulesBean( - var type: String = "field", - var ip: ArrayList? = null, - var domain: ArrayList? = null, - var outboundTag: String = "", - var balancerTag: String? = null, - var port: String? = null, - val sourcePort: String? = null, - val network: String? = null, - val source: List? = null, - val user: List? = null, - var inboundTag: List? = null, - val protocol: List? = null, - val attrs: String? = null, - val domainMatcher: String? = null - ) - } - - data class PolicyBean( - var levels: Map, - var system: Any? = null - ) { - data class LevelBean( - var handshake: Int? = null, - var connIdle: Int? = null, - var uplinkOnly: Int? = null, - var downlinkOnly: Int? = null, - val statsUserUplink: Boolean? = null, - val statsUserDownlink: Boolean? = null, - var bufferSize: Int? = null - ) - } - - data class FakednsBean( - var ipPool: String = "198.18.0.0/15", - var poolSize: Int = 10000 - ) // roughly 10 times smaller than total ip pool - - fun getProxyOutbound(): OutboundBean? { - outbounds.forEach { outbound -> - EConfigType.entries.forEach { - if (outbound.protocol.equals(it.name, true)) { - return outbound - } - } - } - return null - } - - fun getAllProxyOutbound(): List { - return outbounds.filter { outbound -> - EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) } - } - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt deleted file mode 100644 index 779c7e3c..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.v2ray.ang.dto - -data class VmessQRCode( - var v: String = "", - var ps: String = "", - var add: String = "", - var port: String = "", - var id: String = "", - var aid: String = "0", - var scy: String = "", - var net: String = "", - var type: String = "", - var host: String = "", - var path: String = "", - var tls: String = "", - var sni: String = "", - var alpn: String = "", - var fp: String = "" -) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt deleted file mode 100644 index 6b7bc379..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.v2ray.ang.dto - -/** - * VPN interface address configuration enum class - * Defines predefined IPv4 and IPv6 address pairs for VPN TUN interface configuration. - * Each option provides client and router addresses to establish point-to-point VPN tunnels. - */ -enum class VpnInterfaceAddressConfig( - val displayName: String, - val ipv4Client: String, - val ipv4Router: String, - val ipv6Client: String, - val ipv6Router: String -) { - OPTION_1("10.10.14.x", "10.10.14.1", "10.10.14.2", "fc00::10:10:14:1", "fc00::10:10:14:2"), - OPTION_2("10.1.0.x", "10.1.0.1", "10.1.0.2", "fc00::10:1:0:1", "fc00::10:1:0:2"), - OPTION_3("10.0.0.x", "10.0.0.1", "10.0.0.2", "fc00::10:0:0:1", "fc00::10:0:0:2"), - OPTION_4("172.31.0.x", "172.31.0.1", "172.31.0.2", "fc00::172:31:0:1", "fc00::172:31:0:2"), - OPTION_5("172.20.0.x", "172.20.0.1", "172.20.0.2", "fc00::172:20:0:1", "fc00::172:20:0:2"), - OPTION_6("172.16.0.x", "172.16.0.1", "172.16.0.2", "fc00::172:16:0:1", "fc00::172:16:0:2"), - OPTION_7("192.168.100.x", "192.168.100.1", "192.168.100.2", "fc00::192:168:100:1", "fc00::192:168:100:2"); - - companion object { - /** - * Retrieves the VPN interface address configuration based on the specified index. - * - * @param index The configuration index (0-based) corresponding to user selection - * @return The VpnInterfaceAddressConfig instance at the specified index, - * or OPTION_1 (default) if the index is out of bounds - */ - fun getConfigByIndex(index: Int): VpnInterfaceAddressConfig { - return if (index in values().indices) { - values()[index] - } else { - OPTION_1 // Default to the first configuration - } - } - } -} 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 deleted file mode 100644 index 6e5c6bb4..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt +++ /dev/null @@ -1,212 +0,0 @@ -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 es.dmoral.toasty.Toasty -import org.json.JSONObject -import java.io.Serializable -import java.net.URI -import java.net.URLConnection - -val Context.v2RayApplication: AngApplication? - get() = applicationContext as? AngApplication - -/** - * Shows a toast message with the given resource ID. - * - * @param message The resource ID of the message to show. - */ -fun Context.toast(message: Int) { - Toasty.normal(this, message).show() -} - -/** - * Shows a toast message with the given text. - * - * @param message The text of the message to show. - */ -fun Context.toast(message: CharSequence) { - Toasty.normal(this, message).show() -} - -/** - * Shows a toast message with the given resource ID. - * - * @param message The resource ID of the message to show. - */ -fun Context.toastSuccess(message: Int) { - Toasty.success(this, message, Toast.LENGTH_SHORT, true).show() -} - -/** - * Shows a toast message with the given text. - * - * @param message The text of the message to show. - */ -fun Context.toastSuccess(message: CharSequence) { - Toasty.success(this, message, Toast.LENGTH_SHORT, true).show() -} - -/** - * Shows a toast message with the given resource ID. - * - * @param message The resource ID of the message to show. - */ -fun Context.toastError(message: Int) { - Toasty.error(this, message, Toast.LENGTH_SHORT, true).show() -} - -/** - * Shows a toast message with the given text. - * - * @param message The text of the message to show. - */ -fun Context.toastError(message: CharSequence) { - Toasty.error(this, message, Toast.LENGTH_SHORT, true).show() -} - - -/** - * Puts a key-value pair into the JSONObject. - * - * @param pair The key-value pair to put. - */ -fun JSONObject.putOpt(pair: Pair) { - put(pair.first, pair.second) -} - -/** - * Puts multiple key-value pairs into the JSONObject. - * - * @param pairs The map of key-value pairs to put. - */ -fun JSONObject.putOpt(pairs: Map) { - pairs.forEach { put(it.key, it.value) } -} - -const val THRESHOLD = 1000L -const val DIVISOR = 1024.0 - -/** - * Converts a Long value to a speed string. - * - * @return The speed string. - */ -fun Long.toSpeedString(): String = this.toTrafficString() + "/s" - -/** - * Converts a Long value to a traffic string. - * - * @return The traffic string. - */ -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() - -/** - * Removes all whitespace from the string. - * - * @return The string without whitespace. - */ -fun String?.removeWhiteSpace(): String? = this?.replace(" ", "") - -/** - * Converts the string to a Long value, or returns 0 if the conversion fails. - * - * @return The Long value. - */ -fun String.toLongEx(): Long = toLongOrNull() ?: 0 - -/** - * Listens for package changes and executes a callback when a change occurs. - * - * @param onetime Whether to unregister the receiver after the first callback. - * @param callback The callback to execute when a package change occurs. - * @return The BroadcastReceiver that was registered. - */ -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") - }) - } - } - -/** - * Retrieves a serializable object from the Bundle. - * - * @param key The key of the serializable object. - * @return The serializable object, or null if not found. - */ -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 -} - -/** - * Retrieves a serializable object from the Intent. - * - * @param key The key of the serializable object. - * @return The serializable object, or null if not found. - */ -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 -} - -/** - * Checks if the CharSequence is not null and not empty. - * - * @return True if the CharSequence is not null and not empty, false otherwise. - */ -fun CharSequence?.isNotNullEmpty(): Boolean = this != null && this.isNotEmpty() - -fun String.concatUrl(vararg paths: String): String { - val builder = StringBuilder(this.trimEnd('/')) - - paths.forEach { path -> - val trimmedPath = path.trim('/') - if (trimmedPath.isNotEmpty()) { - builder.append('/').append(trimmedPath) - } - } - - return builder.toString() -} \ 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 deleted file mode 100644 index 3bc20927..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/CustomFmt.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.v2ray.ang.fmt - -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ProfileItem -import com.v2ray.ang.dto.V2rayConfig -import com.v2ray.ang.util.JsonUtil - -object CustomFmt : FmtBase() { - /** - * Parses a JSON string into a ProfileItem object. - * - * @param str the JSON string to parse - * @return the parsed ProfileItem object, or null if parsing fails - */ - 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 deleted file mode 100644 index 73cdf958..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt +++ /dev/null @@ -1,170 +0,0 @@ -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.handler.MmkvManager -import com.v2ray.ang.util.HttpUtil -import com.v2ray.ang.util.Utils -import java.net.URI - -open class FmtBase { - /** - * Converts a ProfileItem object to a URI string. - * - * @param config the ProfileItem object to convert - * @param userInfo the user information to include in the URI - * @param dicQuery the query parameters to include in the URI - * @return the converted URI string - */ - 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(HttpUtil.toIdnDomain(config.server.orEmpty())), - config.serverPort - ) - - return "${url}${query}#${Utils.urlEncode(config.remarks)}" - } - - /** - * Extracts query parameters from a URI. - * - * @param uri the URI to extract query parameters from - * @return a map of query parameters - */ - fun getQueryParam(uri: URI): Map { - return uri.rawQuery.split("&") - .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } - } - - /** - * Populates a ProfileItem object with values from query parameters. - * - * @param config the ProfileItem object to populate - * @param queryParam the query parameters to use for populating the ProfileItem - * @param allowInsecure whether to allow insecure connections - */ - 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"] - } - - /** - * Creates a map of query parameters from a ProfileItem object. - * - * @param config the ProfileItem object to create query parameters from - * @return a map of query parameters - */ - 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 - } - - fun getServerAddress(profileItem: ProfileItem): String { - if (Utils.isPureIpAddress(profileItem.server.orEmpty())) { - return profileItem.server.orEmpty() - } - - val domain = HttpUtil.toIdnDomain(profileItem.server.orEmpty()) - if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") != "2") { - return domain - } - //Resolve and replace domain - val resolvedIps = HttpUtil.resolveHostToIP(domain, MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) - if (resolvedIps.isNullOrEmpty()) { - return domain - } - return resolvedIps.first() - } -} 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 deleted file mode 100644 index 8c641f24..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.v2ray.ang.fmt - -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ProfileItem -import com.v2ray.ang.dto.V2rayConfig.OutboundBean -import com.v2ray.ang.extension.isNotNullEmpty -import com.v2ray.ang.handler.V2rayConfigManager - -object HttpFmt : FmtBase() { - /** - * Converts a ProfileItem object to an OutboundBean object. - * - * @param profileItem the ProfileItem object to convert - * @return the converted OutboundBean object, or null if conversion fails - */ - fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HTTP) - - outboundBean?.settings?.servers?.first()?.let { server -> - server.address = getServerAddress(profileItem) - 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 deleted file mode 100644 index 3b3dc88c..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt +++ /dev/null @@ -1,151 +0,0 @@ -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.handler.V2rayConfigManager -import com.v2ray.ang.util.Utils -import java.net.URI - -object Hysteria2Fmt : FmtBase() { - /** - * Parses a Hysteria2 URI string into a ProfileItem object. - * - * @param str the Hysteria2 URI string to parse - * @return the parsed ProfileItem object, or null if parsing fails - */ - 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()).let { if (it.isEmpty()) "none" else it } - 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 - } - - /** - * Converts a ProfileItem object to a URI string. - * - * @param config the ProfileItem object to convert - * @return the converted URI string - */ - 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) - } - - /** - * Converts a ProfileItem object to a Hysteria2Bean object. - * - * @param config the ProfileItem object to convert - * @param socksPort the port number for the socks5 proxy - * @return the converted Hysteria2Bean object, or null if conversion fails - */ - fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? { - - val obfs = if (config.obfsPassword.isNullOrEmpty()) null else - Hysteria2Bean.ObfsBean( - type = "salamander", - salamander = Hysteria2Bean.ObfsBean.SalamanderBean( - password = config.obfsPassword - ) - ) - - val transport = if (config.portHopping.isNullOrEmpty()) null else - Hysteria2Bean.TransportBean( - type = "udp", - udp = Hysteria2Bean.TransportBean.TransportUdpBean( - hopInterval = (config.portHoppingInterval ?: "30") + "s" - ) - ) - - val bandwidth = if (config.bandwidthDown.isNullOrEmpty() || config.bandwidthUp.isNullOrEmpty()) null else - Hysteria2Bean.BandwidthBean( - down = config.bandwidthDown, - up = config.bandwidthUp, - ) - - val server = - if (config.portHopping.isNullOrEmpty()) - config.getServerAddressAndPort() - else - Utils.getIpv6Address(config.server) + ":" + config.portHopping - - val bean = Hysteria2Bean( - server = server, - auth = config.password, - obfs = obfs, - transport = transport, - bandwidth = bandwidth, - socks5 = Hysteria2Bean.Socks5Bean( - listen = "$LOOPBACK:${socksPort}", - ), - http = Hysteria2Bean.Socks5Bean( - listen = "$LOOPBACK:${socksPort}", - ), - tls = Hysteria2Bean.TlsBean( - sni = config.sni ?: config.server, - insecure = config.insecure, - pinSHA256 = if (config.pinSHA256.isNullOrEmpty()) null else config.pinSHA256 - ) - ) - return bean - } - - /** - * Converts a ProfileItem object to an OutboundBean object. - * - * @param profileItem the ProfileItem object to convert - * @return the converted OutboundBean object, or null if conversion fails - */ - fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = V2rayConfigManager.createInitOutbound(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 deleted file mode 100644 index 87ba74f8..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.v2ray.ang.fmt - -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.extension.idnHost -import com.v2ray.ang.handler.V2rayConfigManager -import com.v2ray.ang.util.Utils -import java.net.URI - -object ShadowsocksFmt : FmtBase() { - /** - * Parses a Shadowsocks URI string into a ProfileItem object. - * - * @param str the Shadowsocks URI string to parse - * @return the parsed ProfileItem object, or null if parsing fails - */ - fun parse(str: String): ProfileItem? { - return parseSip002(str) ?: parseLegacy(str) - } - - /** - * Parses a SIP002 Shadowsocks URI string into a ProfileItem object. - * - * @param str the SIP002 Shadowsocks URI string to parse - * @return the parsed ProfileItem object, or null if parsing fails - */ - 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()).let { if (it.isEmpty()) "none" else it } - 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 - } - - /** - * Parses a legacy Shadowsocks URI string into a ProfileItem object. - * - * @param str the legacy Shadowsocks URI string to parse - * @return the parsed ProfileItem object, or null if parsing fails - */ - 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) { - Log.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e) - } - - 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 - } - - /** - * Converts a ProfileItem object to a URI string. - * - * @param config the ProfileItem object to convert - * @return the converted URI string - */ - fun toUri(config: ProfileItem): String { - val pw = "${config.method}:${config.password}" - - return toUri(config, Utils.encode(pw), null) - } - - /** - * Converts a ProfileItem object to an OutboundBean object. - * - * @param profileItem the ProfileItem object to convert - * @return the converted OutboundBean object, or null if conversion fails - */ - fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS) - - outboundBean?.settings?.servers?.first()?.let { server -> - server.address = getServerAddress(profileItem) - server.port = profileItem.serverPort.orEmpty().toInt() - server.password = profileItem.password - server.method = profileItem.method - } - - val sni = outboundBean?.streamSettings?.let { - V2rayConfigManager.populateTransportSettings(it, profileItem) - } - - outboundBean?.streamSettings?.let { - V2rayConfigManager.populateTlsSettings(it, profileItem, sni) - } - - 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 deleted file mode 100644 index 30bc08e4..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.v2ray.ang.fmt - -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ProfileItem -import com.v2ray.ang.dto.V2rayConfig.OutboundBean -import com.v2ray.ang.extension.idnHost -import com.v2ray.ang.extension.isNotNullEmpty -import com.v2ray.ang.handler.V2rayConfigManager -import com.v2ray.ang.util.Utils -import java.net.URI - -object SocksFmt : FmtBase() { - /** - * Parses a Socks URI string into a ProfileItem object. - * - * @param str the Socks URI string to parse - * @return the parsed ProfileItem object, or null if parsing fails - */ - 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()).let { if (it.isEmpty()) "none" else it } - 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 - } - - /** - * Converts a ProfileItem object to a URI string. - * - * @param config the ProfileItem object to convert - * @return the converted URI string - */ - fun toUri(config: ProfileItem): String { - val pw = - if (config.username.isNotNullEmpty()) - "${config.username}:${config.password}" - else - ":" - - return toUri(config, Utils.encode(pw), null) - } - - /** - * Converts a ProfileItem object to an OutboundBean object. - * - * @param profileItem the ProfileItem object to convert - * @return the converted OutboundBean object, or null if conversion fails - */ - fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS) - - outboundBean?.settings?.servers?.first()?.let { server -> - server.address = getServerAddress(profileItem) - 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 deleted file mode 100644 index 446ef99c..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt +++ /dev/null @@ -1,83 +0,0 @@ -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.handler.V2rayConfigManager -import com.v2ray.ang.util.Utils -import java.net.URI - -object TrojanFmt : FmtBase() { - /** - * Parses a Trojan URI string into a ProfileItem object. - * - * @param str the Trojan URI string to parse - * @return the parsed ProfileItem object, or null if parsing fails - */ - 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()).let { if (it.isEmpty()) "none" else it } - 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 - } - - /** - * Converts a ProfileItem object to a URI string. - * - * @param config the ProfileItem object to convert - * @return the converted URI string - */ - fun toUri(config: ProfileItem): String { - val dicQuery = getQueryDic(config) - - return toUri(config, config.password, dicQuery) - } - - /** - * Converts a ProfileItem object to an OutboundBean object. - * - * @param profileItem the ProfileItem object to convert - * @return the converted OutboundBean object, or null if conversion fails - */ - fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN) - - outboundBean?.settings?.servers?.first()?.let { server -> - server.address = getServerAddress(profileItem) - server.port = profileItem.serverPort.orEmpty().toInt() - server.password = profileItem.password - server.flow = profileItem.flow - } - - val sni = outboundBean?.streamSettings?.let { - V2rayConfigManager.populateTransportSettings(it, profileItem) - } - - outboundBean?.streamSettings?.let { - V2rayConfigManager.populateTlsSettings(it, profileItem, sni) - } - - 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 deleted file mode 100644 index 9242f0ec..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt +++ /dev/null @@ -1,80 +0,0 @@ -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.handler.V2rayConfigManager -import com.v2ray.ang.util.Utils -import java.net.URI - -object VlessFmt : FmtBase() { - - /** - * Parses a Vless URI string into a ProfileItem object. - * - * @param str the Vless URI string to parse - * @return the parsed ProfileItem object, or null if parsing fails - */ - 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()).let { if (it.isEmpty()) "none" else it } - config.server = uri.idnHost - config.serverPort = uri.port.toString() - config.password = uri.userInfo - config.method = queryParam["encryption"] ?: "none" - - getItemFormQuery(config, queryParam, allowInsecure) - - return config - } - - /** - * Converts a ProfileItem object to a URI string. - * - * @param config the ProfileItem object to convert - * @return the converted URI string - */ - fun toUri(config: ProfileItem): String { - val dicQuery = getQueryDic(config) - dicQuery["encryption"] = config.method ?: "none" - - return toUri(config, config.password, dicQuery) - } - - /** - * Converts a ProfileItem object to an OutboundBean object. - * - * @param profileItem the ProfileItem object to convert - * @return the converted OutboundBean object, or null if conversion fails - */ - fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VLESS) - - outboundBean?.settings?.vnext?.first()?.let { vnext -> - vnext.address = getServerAddress(profileItem) - 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?.let { - V2rayConfigManager.populateTransportSettings(it, profileItem) - } - - outboundBean?.streamSettings?.let { - V2rayConfigManager.populateTlsSettings(it, profileItem, sni) - } - - 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 deleted file mode 100644 index 4201f4dc..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt +++ /dev/null @@ -1,192 +0,0 @@ -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.handler.V2rayConfigManager -import com.v2ray.ang.util.JsonUtil -import com.v2ray.ang.util.Utils -import java.net.URI - -object VmessFmt : FmtBase() { - /** - * Parses a Vmess string into a ProfileItem object. - * - * @param str the Vmess string to parse - * @return the parsed ProfileItem object, or null if parsing fails - */ - 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.w(AppConfig.TAG, "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.w(AppConfig.TAG, "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 - } - - /** - * Converts a ProfileItem object to a URI string. - * - * @param config the ProfileItem object to convert - * @return the converted URI string - */ - 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) - } - - /** - * Parses a standard Vmess URI string into a ProfileItem object. - * - * @param str the standard Vmess URI string to parse - * @return the parsed ProfileItem object, or null if parsing fails - */ - 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()).let { if (it.isEmpty()) "none" else it } - config.server = uri.idnHost - config.serverPort = uri.port.toString() - config.password = uri.userInfo - config.method = AppConfig.DEFAULT_SECURITY - - getItemFormQuery(config, queryParam, allowInsecure) - - return config - } - - /** - * Converts a ProfileItem object to an OutboundBean object. - * - * @param profileItem the ProfileItem object to convert - * @return the converted OutboundBean object, or null if conversion fails - */ - fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VMESS) - - outboundBean?.settings?.vnext?.first()?.let { vnext -> - vnext.address = getServerAddress(profileItem) - vnext.port = profileItem.serverPort.orEmpty().toInt() - vnext.users[0].id = profileItem.password.orEmpty() - vnext.users[0].security = profileItem.method - } - - val sni = outboundBean?.streamSettings?.let { - V2rayConfigManager.populateTransportSettings(it, profileItem) - } - - outboundBean?.streamSettings?.let { - V2rayConfigManager.populateTlsSettings(it, profileItem, sni) - } - - 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 deleted file mode 100644 index 8f1cec84..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt +++ /dev/null @@ -1,149 +0,0 @@ -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.removeWhiteSpace -import com.v2ray.ang.handler.V2rayConfigManager -import com.v2ray.ang.util.Utils -import java.net.URI - -object WireguardFmt : FmtBase() { - /** - * Parses a URI string into a ProfileItem object. - * - * @param str the URI string to parse - * @return the parsed ProfileItem object, or null if parsing fails - */ - 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()).let { if (it.isEmpty()) "none" else it } - 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"]?.takeIf { it.isNotEmpty() } - config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU) - config.reserved = queryParam["reserved"] ?: "0,0,0" - - return config - } - - /** - * Parses a Wireguard configuration file string into a ProfileItem object. - * - * @param str the Wireguard configuration file string to parse - * @return the parsed ProfileItem object, or null if parsing fails - */ - fun parseWireguardConfFile(str: String): ProfileItem? { - val config = ProfileItem.create(EConfigType.WIREGUARD) - - val interfaceParams: MutableMap = mutableMapOf() - val peerParams: MutableMap = mutableMapOf() - - var currentSection: String? = null - - str.lines().forEach { line -> - val trimmedLine = line.trim() - - if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) { - return@forEach - } - - when { - trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface" - trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer" - else -> { - if (currentSection != null) { - val parts = trimmedLine.split("=", limit = 2).map { it.trim() } - if (parts.size == 2) { - val key = parts[0].lowercase() - val value = parts[1] - when (currentSection) { - "Interface" -> interfaceParams[key] = value - "Peer" -> peerParams[key] = value - } - } - } - } - } - } - - config.secretKey = interfaceParams["privatekey"].orEmpty() - config.remarks = System.currentTimeMillis().toString() - config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4 - config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU) - config.publicKey = peerParams["publickey"].orEmpty() - config.preSharedKey = peerParams["presharedkey"]?.takeIf { it.isNotEmpty() } - val endpoint = peerParams["endpoint"].orEmpty() - val endpointParts = endpoint.split(":", limit = 2) - if (endpointParts.size == 2) { - config.server = endpointParts[0] - config.serverPort = endpointParts[1] - } else { - config.server = endpoint - config.serverPort = "" - } - config.reserved = peerParams["reserved"] ?: "0,0,0" - - return config - } - - /** - * Converts a ProfileItem object to an OutboundBean object. - * - * @param profileItem the ProfileItem object to convert - * @return the converted OutboundBean object, or null if conversion fails - */ - fun toOutbound(profileItem: ProfileItem): OutboundBean? { - val outboundBean = V2rayConfigManager.createInitOutbound(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?.takeIf { it.isNotEmpty() } - peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}" - } - wireguard.mtu = profileItem.mtu - wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() } - } - - return outboundBean - } - - /** - * Converts a ProfileItem object to a URI string. - * - * @param config the ProfileItem object to convert - * @return the converted URI string - */ - fun toUri(config: ProfileItem): String { - val dicQuery = HashMap() - - dicQuery["publickey"] = config.publicKey.orEmpty() - if (config.reserved != null) { - dicQuery["reserved"] = config.reserved.removeWhiteSpace().orEmpty() - } - dicQuery["address"] = config.localAddress.removeWhiteSpace().orEmpty() - if (config.mtu != null) { - dicQuery["mtu"] = config.mtu.toString() - } - if (config.preSharedKey != null) { - dicQuery["presharedkey"] = config.preSharedKey.removeWhiteSpace().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 deleted file mode 100644 index d24ae0c2..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt +++ /dev/null @@ -1,493 +0,0 @@ -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.EConfigType -import com.v2ray.ang.dto.ProfileItem -import com.v2ray.ang.dto.SubscriptionItem -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.HttpUtil -import com.v2ray.ang.util.JsonUtil -import com.v2ray.ang.util.QRCodeDecoder -import com.v2ray.ang.util.Utils -import java.net.URI - -object AngConfigManager { - - - /** - * Shares the configuration to the clipboard. - * - * @param context The context. - * @param guid The GUID of the configuration. - * @return The result code. - */ - 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) { - Log.e(AppConfig.TAG, "Failed to share config to clipboard", e) - return -1 - } - return 0 - } - - /** - * Shares non-custom configurations to the clipboard. - * - * @param context The context. - * @param serverList The list of server GUIDs. - * @return The number of configurations shared. - */ - 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) { - Log.e(AppConfig.TAG, "Failed to share non-custom configs to clipboard", e) - return -1 - } - } - - /** - * Shares the configuration as a QR code. - * - * @param guid The GUID of the configuration. - * @return The QR code bitmap. - */ - fun share2QRCode(guid: String): Bitmap? { - try { - val conf = shareConfig(guid) - if (TextUtils.isEmpty(conf)) { - return null - } - return QRCodeDecoder.createQRCode(conf) - - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to share config as QR code", e) - return null - } - } - - /** - * Shares the full content of the configuration to the clipboard. - * - * @param context The context. - * @param guid The GUID of the configuration. - * @return The result code. - */ - 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) { - Log.e(AppConfig.TAG, "Failed to share full content to clipboard", e) - return -1 - } - return 0 - } - - /** - * Shares the configuration. - * - * @param guid The GUID of the configuration. - * @return The configuration string. - */ - 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) { - Log.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e) - return "" - } - } - - /** - * Imports a batch of configurations. - * - * @param server The server string. - * @param subid The subscription ID. - * @param append Whether to append the configurations. - * @return A pair containing the number of configurations and subscriptions imported. - */ - 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 - } - - /** - * Parses a batch of subscriptions. - * - * @param servers The servers string. - * @return The number of subscriptions parsed. - */ - private 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) { - Log.e(AppConfig.TAG, "Failed to parse batch subscription", e) - } - return 0 - } - - /** - * Parses a batch of configurations. - * - * @param servers The servers string. - * @param subid The subscription ID. - * @param append Whether to append the configurations. - * @return The number of configurations parsed. - */ - private 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) { - Log.e(AppConfig.TAG, "Failed to parse batch config", e) - } - return 0 - } - - /** - * Parses a custom configuration server. - * - * @param server The server string. - * @param subid The subscription ID. - * @return The number of configurations parsed. - */ - private 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) { - Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e) - } - - 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) { - Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e) - } - 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) { - Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e) - } - return 0 - } else { - return 0 - } - } - - /** - * Parses the configuration from a QR code or string. - * - * @param str The configuration string. - * @param subid The subscription ID. - * @param subItem The subscription item. - * @param removedSelectedServer The removed selected server. - * @return The result code. - */ - 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) { - Log.e(AppConfig.TAG, "Failed to parse config", e) - return -1 - } - return 0 - } - - /** - * Updates the configuration via all subscriptions. - * - * @return The number of configurations updated. - */ - fun updateConfigViaSubAll(): Int { - var count = 0 - try { - MmkvManager.decodeSubscriptions().forEach { - count += updateConfigViaSub(it) - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e) - return 0 - } - return count - } - - /** - * Updates the configuration via a subscription. - * - * @param it The subscription item. - * @return The number of configurations updated. - */ - 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 = HttpUtil.toIdnUrl(it.second.url) - if (!Utils.isValidUrl(url)) { - return 0 - } - if (!it.second.allowInsecureUrl) { - if (!Utils.isValidSubUrl(url)) { - return 0 - } - } - Log.i(AppConfig.TAG, url) - - var configText = try { - val httpPort = SettingsManager.getHttpPort() - HttpUtil.getUrlContentWithUserAgent(url, 15000, httpPort) - } catch (e: Exception) { - Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e) - "" - } - if (configText.isEmpty()) { - configText = try { - HttpUtil.getUrlContentWithUserAgent(url) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e) - "" - } - } - if (configText.isEmpty()) { - return 0 - } - return parseConfigViaSub(configText, it.first, false) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to update config via subscription", e) - return 0 - } - } - - /** - * Parses the configuration via a subscription. - * - * @param server The server string. - * @param subid The subscription ID. - * @param append Whether to append the configurations. - * @return The number of configurations parsed. - */ - 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 - } - - /** - * Imports a URL as a subscription. - * - * @param url The URL. - * @return The number of subscriptions imported. - */ - 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 deleted file mode 100644 index 9dd90c76..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MigrateManager.kt +++ /dev/null @@ -1,242 +0,0 @@ -package com.v2ray.ang.handler - -import android.util.Log -import com.tencent.mmkv.MMKV -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.ServerConfig -import com.v2ray.ang.extension.removeWhiteSpace -import com.v2ray.ang.handler.MmkvManager.decodeServerConfig -import com.v2ray.ang.util.JsonUtil - -object MigrateManager { - private const val ID_SERVER_CONFIG = "SERVER_CONFIG" - private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) } - - /** - * Migrates server configurations to profile items. - * - * @return True if migration was successful, false otherwise. - */ - fun migrateServerConfig2Profile(): Boolean { - if (serverStorage.count().toInt() == 0) { - return false - } - val serverList = serverStorage.allKeys() ?: return false - Log.i(AppConfig.TAG, "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.i(AppConfig.TAG, "migrateServerConfig2Profile-" + config.remarks) - } - Log.i(AppConfig.TAG, "migrateServerConfig2Profile-end") - return true - } - - /** - * Migrates a server configuration to a profile item. - * - * @param configOld The old server configuration. - * @return The profile item. - */ - 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 - } - } - - /** - * Migrates a common server configuration to a profile item. - * - * @param configOld The old server configuration. - * @return The profile item. - */ - 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 = tlsSettings?.alpn?.joinToString(",").removeWhiteSpace().toString() - - config.publicKey = tlsSettings?.publicKey - config.shortId = tlsSettings?.shortId - config.spiderX = tlsSettings?.spiderX - - return config - } - - /** - * Migrates a SOCKS server configuration to a profile item. - * - * @param configOld The old server configuration. - * @return The profile item. - */ - 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 - } - - /** - * Migrates an HTTP server configuration to a profile item. - * - * @param configOld The old server configuration. - * @return The profile item. - */ - 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 - } - - /** - * Migrates a WireGuard server configuration to a profile item. - * - * @param configOld The old server configuration. - * @return The profile item. - */ - 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 = (wireguard.address as List<*>).joinToString(",").removeWhiteSpace().toString() - config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey - config.mtu = wireguard.mtu - config.reserved = wireguard.reserved?.joinToString(",").removeWhiteSpace().toString() - } - return config - } - - /** - * Migrates a Hysteria2 server configuration to a profile item. - * - * @param configOld The old server configuration. - * @return The profile item. - */ - 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 = tlsSetting.alpn?.joinToString(",").removeWhiteSpace().orEmpty() - - } - config.obfsPassword = outbound.settings?.obfsPassword - - return config - } - - /** - * Migrates a custom server configuration to a profile item. - * - * @param configOld The old server configuration. - * @return The profile item. - */ - 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 - } - - /** - * Decodes the old server configuration. - * - * @param guid The server GUID. - * @return The old server configuration. - */ - 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 deleted file mode 100644 index 9b589dd0..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt +++ /dev/null @@ -1,588 +0,0 @@ -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 - - /** - * Gets the selected server GUID. - * - * @return The selected server GUID. - */ - fun getSelectServer(): String? { - return mainStorage.decodeString(KEY_SELECTED_SERVER) - } - - /** - * Sets the selected server GUID. - * - * @param guid The server GUID. - */ - fun setSelectServer(guid: String) { - mainStorage.encode(KEY_SELECTED_SERVER, guid) - } - - /** - * Encodes the server list. - * - * @param serverList The list of server GUIDs. - */ - fun encodeServerList(serverList: MutableList) { - mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList)) - } - - /** - * Decodes the server list. - * - * @return The list of server GUIDs. - */ - fun decodeServerList(): MutableList { - val json = mainStorage.decodeString(KEY_ANG_CONFIGS) - return if (json.isNullOrBlank()) { - mutableListOf() - } else { - JsonUtil.fromJson(json, Array::class.java).toMutableList() - } - } - - /** - * Decodes the server configuration. - * - * @param guid The server GUID. - * @return The server configuration. - */ - 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) -// } - - /** - * Encodes the server configuration. - * - * @param guid The server GUID. - * @param config The server configuration. - * @return The server GUID. - */ - 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 - } - - /** - * Removes the server configuration. - * - * @param guid The server GUID. - */ - 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) - } - - /** - * Removes the server configurations via subscription ID. - * - * @param subid The subscription ID. - */ - fun removeServerViaSubid(subid: String) { - if (subid.isBlank()) { - return - } - profileFullStorage.allKeys()?.forEach { key -> - decodeServerConfig(key)?.let { config -> - if (config.subscriptionId == subid) { - removeServer(key) - } - } - } - } - - /** - * Decodes the server affiliation information. - * - * @param guid The server GUID. - * @return The server affiliation information. - */ - 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) - } - - /** - * Encodes the server test delay in milliseconds. - * - * @param guid The server GUID. - * @param testResult The test delay in milliseconds. - */ - fun encodeServerTestDelayMillis(guid: String, testResult: Long) { - if (guid.isBlank()) { - return - } - val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo() - aff.testDelayMillis = testResult - serverAffStorage.encode(guid, JsonUtil.toJson(aff)) - } - - /** - * Clears all test delay results. - * - * @param keys The list of server GUIDs. - */ - fun clearAllTestDelayResults(keys: List?) { - keys?.forEach { key -> - decodeServerAffiliationInfo(key)?.let { aff -> - aff.testDelayMillis = 0 - serverAffStorage.encode(key, JsonUtil.toJson(aff)) - } - } - } - - /** - * Removes all server configurations. - * - * @return The number of server configurations removed. - */ - fun removeAllServer(): Int { - val count = profileFullStorage.allKeys()?.count() ?: 0 - mainStorage.clearAll() - profileFullStorage.clearAll() - //profileStorage.clearAll() - serverAffStorage.clearAll() - return count - } - - /** - * Removes invalid server configurations. - * - * @param guid The server GUID. - * @return The number of server configurations removed. - */ - 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 - } - - /** - * Encodes the raw server configuration. - * - * @param guid The server GUID. - * @param config The raw server configuration. - */ - fun encodeServerRaw(guid: String, config: String) { - serverRawStorage.encode(guid, config) - } - - /** - * Decodes the raw server configuration. - * - * @param guid The server GUID. - * @return The raw server configuration. - */ - fun decodeServerRaw(guid: String): String? { - return serverRawStorage.decodeString(guid) - } - - //endregion - - //region Subscriptions - - /** - * Initializes the subscription list. - */ - private fun initSubsList() { - val subsList = decodeSubsList() - if (subsList.isNotEmpty()) { - return - } - subStorage.allKeys()?.forEach { key -> - subsList.add(key) - } - encodeSubsList(subsList) - } - - /** - * Decodes the subscriptions. - * - * @return The list of subscriptions. - */ - 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 - } - - /** - * Removes the subscription. - * - * @param subid The subscription ID. - */ - fun removeSubscription(subid: String) { - subStorage.remove(subid) - val subsList = decodeSubsList() - subsList.remove(subid) - encodeSubsList(subsList) - - removeServerViaSubid(subid) - } - - /** - * Encodes the subscription. - * - * @param guid The subscription GUID. - * @param subItem The subscription item. - */ - 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) - } - } - - /** - * Decodes the subscription. - * - * @param subscriptionId The subscription ID. - * @return The subscription item. - */ - fun decodeSubscription(subscriptionId: String): SubscriptionItem? { - val json = subStorage.decodeString(subscriptionId) ?: return null - return JsonUtil.fromJson(json, SubscriptionItem::class.java) - } - - /** - * Encodes the subscription list. - * - * @param subsList The list of subscription IDs. - */ - fun encodeSubsList(subsList: MutableList) { - mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList)) - } - - /** - * Decodes the subscription list. - * - * @return The list of subscription IDs. - */ - 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 - - /** - * Decodes the asset URLs. - * - * @return The list of asset URLs. - */ - 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 } - } - - /** - * Removes the asset URL. - * - * @param assetid The asset ID. - */ - fun removeAssetUrl(assetid: String) { - assetStorage.remove(assetid) - } - - /** - * Encodes the asset. - * - * @param assetid The asset ID. - * @param assetItem The asset item. - */ - fun encodeAsset(assetid: String, assetItem: AssetUrlItem) { - val key = assetid.ifBlank { Utils.getUuid() } - assetStorage.encode(key, JsonUtil.toJson(assetItem)) - } - - /** - * Decodes the asset. - * - * @param assetid The asset ID. - * @return The asset item. - */ - fun decodeAsset(assetid: String): AssetUrlItem? { - val json = assetStorage.decodeString(assetid) ?: return null - return JsonUtil.fromJson(json, AssetUrlItem::class.java) - } - - //endregion - - //region Routing - - /** - * Decodes the routing rulesets. - * - * @return The list of routing rulesets. - */ - fun decodeRoutingRulesets(): MutableList? { - val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET) - if (ruleset.isNullOrEmpty()) return null - return JsonUtil.fromJson(ruleset, Array::class.java).toMutableList() - } - - /** - * Encodes the routing rulesets. - * - * @param rulesetList The list of routing rulesets. - */ - fun encodeRoutingRulesets(rulesetList: MutableList?) { - if (rulesetList.isNullOrEmpty()) - encodeSettings(PREF_ROUTING_RULESET, "") - else - encodeSettings(PREF_ROUTING_RULESET, JsonUtil.toJson(rulesetList)) - } - - //endregion - - /** - * Encodes the settings. - * - * @param key The settings key. - * @param value The settings value. - * @return Whether the encoding was successful. - */ - fun encodeSettings(key: String, value: String?): Boolean { - return settingsStorage.encode(key, value) - } - - /** - * Encodes the settings. - * - * @param key The settings key. - * @param value The settings value. - * @return Whether the encoding was successful. - */ - fun encodeSettings(key: String, value: Int): Boolean { - return settingsStorage.encode(key, value) - } - - /** - * Encodes the settings. - * - * @param key The settings key. - * @param value The settings value. - * @return Whether the encoding was successful. - */ - fun encodeSettings(key: String, value: Boolean): Boolean { - return settingsStorage.encode(key, value) - } - - /** - * Encodes the settings. - * - * @param key The settings key. - * @param value The settings value. - * @return Whether the encoding was successful. - */ - fun encodeSettings(key: String, value: MutableSet): Boolean { - return settingsStorage.encode(key, value) - } - - /** - * Decodes the settings string. - * - * @param key The settings key. - * @return The settings value. - */ - fun decodeSettingsString(key: String): String? { - return settingsStorage.decodeString(key) - } - - /** - * Decodes the settings string. - * - * @param key The settings key. - * @param defaultValue The default value. - * @return The settings value. - */ - fun decodeSettingsString(key: String, defaultValue: String?): String? { - return settingsStorage.decodeString(key, defaultValue) - } - - /** - * Decodes the settings boolean. - * - * @param key The settings key. - * @return The settings value. - */ - fun decodeSettingsBool(key: String): Boolean { - return settingsStorage.decodeBool(key, false) - } - - /** - * Decodes the settings boolean. - * - * @param key The settings key. - * @param defaultValue The default value. - * @return The settings value. - */ - fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean { - return settingsStorage.decodeBool(key, defaultValue) - } - - /** - * Decodes the settings string set. - * - * @param key The settings key. - * @return The settings value. - */ - fun decodeSettingsStringSet(key: String): MutableSet? { - return settingsStorage.decodeStringSet(key) - } - - //endregion - - //region Others - - /** - * Encodes the start on boot setting. - * - * @param startOnBoot Whether to start on boot. - */ - fun encodeStartOnBoot(startOnBoot: Boolean) { - encodeSettings(PREF_IS_BOOTED, startOnBoot) - } - - /** - * Decodes the start on boot setting. - * - * @return Whether to start on boot. - */ - 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 deleted file mode 100644 index b2e23f7f..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt +++ /dev/null @@ -1,373 +0,0 @@ -package com.v2ray.ang.handler - -import android.content.Context -import android.content.res.AssetManager -import android.text.TextUtils -import android.util.Log -import androidx.appcompat.app.AppCompatDelegate -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.Language -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.dto.VpnInterfaceAddressConfig -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 java.io.File -import java.io.FileOutputStream -import java.util.Collections -import java.util.Locale - -object SettingsManager { - - /** - * Initialize routing rulesets. - * @param context The application context. - */ - fun initRoutingRulesets(context: Context) { - val exist = MmkvManager.decodeRoutingRulesets() - if (exist.isNullOrEmpty()) { - val rulesetList = getPresetRoutingRulesets(context) - MmkvManager.encodeRoutingRulesets(rulesetList) - } - } - - /** - * Get preset routing rulesets. - * @param context The application context. - * @param index The index of the routing type. - * @return A mutable list of RulesetItem. - */ - 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() - } - - /** - * Reset routing rulesets from presets. - * @param context The application context. - * @param index The index of the routing type. - */ - fun resetRoutingRulesetsFromPresets(context: Context, index: Int) { - val rulesetList = getPresetRoutingRulesets(context, index) ?: return - resetRoutingRulesetsCommon(rulesetList) - } - - /** - * Reset routing rulesets. - * @param content The content of the rulesets. - * @return True if successful, false otherwise. - */ - 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) { - Log.e(ANG_PACKAGE, "Failed to reset routing rulesets", e) - return false - } - } - - /** - * Common method to reset routing rulesets. - * @param rulesetList The list of rulesets. - */ - 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) - } - - /** - * Get a routing ruleset by index. - * @param index The index of the ruleset. - * @return The RulesetItem. - */ - fun getRoutingRuleset(index: Int): RulesetItem? { - if (index < 0) return null - - val rulesetList = MmkvManager.decodeRoutingRulesets() - if (rulesetList.isNullOrEmpty()) return null - - return rulesetList[index] - } - - /** - * Save a routing ruleset. - * @param index The index of the ruleset. - * @param ruleset The RulesetItem to save. - */ - 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) - } - - /** - * Remove a routing ruleset by index. - * @param index The index of the ruleset. - */ - fun removeRoutingRuleset(index: Int) { - if (index < 0) return - - val rulesetList = MmkvManager.decodeRoutingRulesets() - if (rulesetList.isNullOrEmpty()) return - - rulesetList.removeAt(index) - MmkvManager.encodeRoutingRulesets(rulesetList) - } - - /** - * Check if routing rulesets bypass LAN. - * @return True if bypassing LAN, false otherwise. - */ - fun routingRulesetsBypassLan(): Boolean { - val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "1" - if (vpnBypassLan == "1") { - return true - } else if (vpnBypassLan == "2") { - return false - } - - val guid = MmkvManager.getSelectServer() ?: return false - val config = 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 - } - - /** - * Swap routing rulesets. - * @param fromPosition The position to swap from. - * @param toPosition The position to swap to. - */ - fun swapRoutingRuleset(fromPosition: Int, toPosition: Int) { - val rulesetList = MmkvManager.decodeRoutingRulesets() - if (rulesetList.isNullOrEmpty()) return - - Collections.swap(rulesetList, fromPosition, toPosition) - MmkvManager.encodeRoutingRulesets(rulesetList) - } - - /** - * Swap subscriptions. - * @param fromPosition The position to swap from. - * @param toPosition The position to swap to. - */ - fun swapSubscriptions(fromPosition: Int, toPosition: Int) { - val subsList = MmkvManager.decodeSubsList() - if (subsList.isNullOrEmpty()) return - - Collections.swap(subsList, fromPosition, toPosition) - MmkvManager.encodeSubsList(subsList) - } - - /** - * Get server via remarks. - * @param remarks The remarks of the server. - * @return The ProfileItem. - */ - fun getServerViaRemarks(remarks: String?): ProfileItem? { - if (remarks.isNullOrEmpty()) { - return null - } - val serverList = decodeServerList() - for (guid in serverList) { - val profile = decodeServerConfig(guid) - if (profile != null && profile.remarks == remarks) { - return profile - } - } - return null - } - - /** - * Get the SOCKS port. - * @return The SOCKS port. - */ - fun getSocksPort(): Int { - return Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt()) - } - - /** - * Get the HTTP port. - * @return The HTTP port. - */ - fun getHttpPort(): Int { - return getSocksPort() + if (Utils.isXray()) 0 else 1 - } - - /** - * Initialize assets. - * @param context The application context. - * @param assets The AssetManager. - */ - fun initAssets(context: Context, assets: AssetManager) { - val extFolder = Utils.userAssetPath(context) - - 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(AppConfig.TAG, "Copied from apk assets folder to ${target.absolutePath}") - } - } catch (e: Exception) { - Log.e(ANG_PACKAGE, "asset copy failed", e) - } - } - - /** - * Get domestic DNS servers from preference. - * @return A list of domestic DNS servers. - */ - fun getDomesticDnsServers(): List { - val domesticDns = - MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT - val ret = domesticDns.split(",").filter { Utils.isPureIpAddress(it) || Utils.isCoreDNSAddress(it) } - if (ret.isEmpty()) { - return listOf(AppConfig.DNS_DIRECT) - } - return ret - } - - /** - * Get remote DNS servers from preference. - * @return A list of remote DNS servers. - */ - fun getRemoteDnsServers(): List { - val remoteDns = - MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY - val ret = remoteDns.split(",").filter { Utils.isPureIpAddress(it) || Utils.isCoreDNSAddress(it) } - if (ret.isEmpty()) { - return listOf(AppConfig.DNS_PROXY) - } - return ret - } - - /** - * Get VPN DNS servers from preference. - * @return A list of VPN DNS servers. - */ - fun getVpnDnsServers(): List { - val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN - return vpnDns.split(",").filter { Utils.isPureIpAddress(it) } - } - - /** - * Get delay test URL. - * @param second Whether to use the second URL. - * @return The delay test URL. - */ - fun getDelayTestUrl(second: Boolean = false): String { - return if (second) { - AppConfig.DELAY_TEST_URL2 - } else { - MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) - ?: AppConfig.DELAY_TEST_URL - } - } - - /** - * Get the locale. - * @return The locale. - */ - fun getLocale(): Locale { - val langCode = - MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code - val language = Language.fromCode(langCode) - - return when (language) { - Language.AUTO -> Utils.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.ARABIC -> Locale("ar") - Language.BANGLA -> Locale("bn") - Language.BAKHTIARI -> Locale("bqi", "IR") - } - } - - /** - * Set night mode. - */ - 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) - } - } - - /** - * Retrieves the currently selected VPN interface address configuration. - * This method reads the user's preference for VPN interface addressing and returns - * the corresponding configuration containing IPv4 and IPv6 addresses. - * - * @return The selected VpnInterfaceAddressConfig instance, or the default configuration - * if no valid selection is found or if the stored index is invalid. - */ - fun getCurrentVpnInterfaceAddressConfig(): VpnInterfaceAddressConfig { - val selectedIndex = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, "0")?.toInt() - return VpnInterfaceAddressConfig.getConfigByIndex(selectedIndex ?: 0) - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt deleted file mode 100644 index e547c378..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt +++ /dev/null @@ -1,189 +0,0 @@ -package com.v2ray.ang.handler - -import android.content.Context -import android.os.SystemClock -import android.text.TextUtils -import android.util.Log -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.dto.IPAPIInfo -import com.v2ray.ang.extension.responseLength -import com.v2ray.ang.util.HttpUtil -import com.v2ray.ang.util.JsonUtil -import kotlinx.coroutines.isActive -import libv2ray.Libv2ray -import java.io.IOException -import java.net.InetSocketAddress -import java.net.Socket -import java.net.UnknownHostException -import kotlin.coroutines.coroutineContext - -object SpeedtestManager { - - private val tcpTestingSockets = ArrayList() - - /** - * Measures the TCP connection time to a given URL and port. - * - * @param url The URL to connect to. - * @param port The port to connect to. - * @return The connection time in milliseconds, or -1 if the connection failed. - */ - suspend fun tcping(url: String, port: Int): Long { - var time = -1L - for (k in 0 until 2) { - val one = socketConnectTime(url, port) - if (!coroutineContext.isActive) { - break - } - if (one != -1L && (time == -1L || one < time)) { - time = one - } - } - return time - } - - /** - * Measures the real ping time using the V2Ray library. - * - * @param config The configuration string for the V2Ray library. - * @return The ping time in milliseconds, or -1 if the ping failed. - */ - fun realPing(config: String): Long { - return try { - Libv2ray.measureOutboundDelay(config, SettingsManager.getDelayTestUrl()) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to measure outbound delay", e) - -1L - } - } - - /** - * Measures the ping time to a given URL using the system ping command. - * - * @param url The URL to ping. - * @return The ping time in milliseconds as a string, or "-1ms" if the ping failed. - */ - fun ping(url: String): String { - try { - val command = "/system/bin/ping -c 3 $url" - val process = Runtime.getRuntime().exec(command) - val allText = process.inputStream.bufferedReader().use { it.readText() } - if (!TextUtils.isEmpty(allText)) { - val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19) - val temps = - tempInfo.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - if (temps.count() > 0 && temps[0].length < 10) { - return temps[0].toFloat().toInt().toString() + "ms" - } - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to ping URL: $url", e) - } - return "-1ms" - } - - /** - * Measures the time taken to establish a TCP connection to a given URL and port. - * - * @param url The URL to connect to. - * @param port The port to connect to. - * @return The connection time in milliseconds, or -1 if the connection failed. - */ - fun socketConnectTime(url: String, port: Int): Long { - try { - val socket = Socket() - synchronized(this) { - tcpTestingSockets.add(socket) - } - val start = System.currentTimeMillis() - socket.connect(InetSocketAddress(url, port), 3000) - val time = System.currentTimeMillis() - start - synchronized(this) { - tcpTestingSockets.remove(socket) - } - socket.close() - return time - } catch (e: UnknownHostException) { - Log.e(AppConfig.TAG, "Unknown host: $url", e) - } catch (e: IOException) { - Log.e(AppConfig.TAG, "socketConnectTime IOException: $e") - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e) - } - return -1 - } - - /** - * Closes all TCP sockets that are currently being tested. - */ - fun closeAllTcpSockets() { - synchronized(this) { - tcpTestingSockets.forEach { - it?.close() - } - tcpTestingSockets.clear() - } - } - - /** - * Tests the connection to a given URL and port. - * - * @param context The Context in which the test is running. - * @param port The port to connect to. - * @return A pair containing the elapsed time in milliseconds and the result message. - */ - fun testConnection(context: Context, port: Int): Pair { - var result: String - var elapsed = -1L - - val conn = HttpUtil.createProxyConnection(SettingsManager.getDelayTestUrl(), port, 15000, 15000) ?: return Pair(elapsed, "") - try { - val start = SystemClock.elapsedRealtime() - val code = conn.responseCode - elapsed = SystemClock.elapsedRealtime() - start - - if (code == 204 || code == 200 && conn.responseLength == 0L) { - result = context.getString(R.string.connection_test_available, elapsed) - } else { - throw IOException( - context.getString( - R.string.connection_test_error_status_code, - code - ) - ) - } - } catch (e: IOException) { - Log.e(AppConfig.TAG, "Connection test IOException", e) - result = context.getString(R.string.connection_test_error, e.message) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Connection test Exception", e) - result = context.getString(R.string.connection_test_error, e.message) - } finally { - conn.disconnect() - } - - return Pair(elapsed, result) - } - - fun getRemoteIPInfo(): String? { - val httpPort = SettingsManager.getHttpPort() - var content = HttpUtil.getUrlContent(AppConfig.IP_API_URL, 5000, httpPort) ?: return null - - var ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null - var ip = ipInfo.ip ?: ipInfo.clientIp ?: ipInfo.ip_addr ?: ipInfo.query - var country = ipInfo.country_code ?: ipInfo.country ?: ipInfo.countryCode - - return "(${country ?: "unknown"}) $ip" - } - - /** - * Gets the version of the V2Ray library. - * - * @return The version of the V2Ray library. - */ - fun getLibVersion(): String { - return Libv2ray.checkVersionX() - } - -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt deleted file mode 100644 index 37b55c2e..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.v2ray.ang.handler - -import android.content.Context -import android.os.Build -import android.util.Log -import com.v2ray.ang.AppConfig -import com.v2ray.ang.BuildConfig -import com.v2ray.ang.dto.CheckUpdateResult -import com.v2ray.ang.dto.GitHubRelease -import com.v2ray.ang.extension.concatUrl -import com.v2ray.ang.util.HttpUtil -import com.v2ray.ang.util.JsonUtil -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileOutputStream - -object UpdateCheckerManager { - suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) { - val url = if (includePreRelease) { - AppConfig.APP_API_URL - } else { - AppConfig.APP_API_URL.concatUrl("latest") - } - - var response = HttpUtil.getUrlContent(url, 5000) - if (response.isNullOrEmpty()) { - val httpPort = SettingsManager.getHttpPort() - response = HttpUtil.getUrlContent(url, 5000, httpPort) ?: throw IllegalStateException("Failed to get response") - } - - val latestRelease = if (includePreRelease) { - JsonUtil.fromJson(response, Array::class.java) - .firstOrNull() - ?: throw IllegalStateException("No pre-release found") - } else { - JsonUtil.fromJson(response, GitHubRelease::class.java) - } - - val latestVersion = latestRelease.tagName.removePrefix("v") - Log.i(AppConfig.TAG, "Found new version: $latestVersion (current: ${BuildConfig.VERSION_NAME})") - - return@withContext if (compareVersions(latestVersion, BuildConfig.VERSION_NAME) > 0) { - val downloadUrl = getDownloadUrl(latestRelease, Build.SUPPORTED_ABIS[0]) - CheckUpdateResult( - hasUpdate = true, - latestVersion = latestVersion, - releaseNotes = latestRelease.body, - downloadUrl = downloadUrl, - isPreRelease = latestRelease.prerelease - ) - } else { - CheckUpdateResult(hasUpdate = false) - } - } - - suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) { - try { - val httpPort = SettingsManager.getHttpPort() - val connection = HttpUtil.createProxyConnection(downloadUrl, httpPort, 10000, 10000, true) - ?: throw IllegalStateException("Failed to create connection") - - try { - val apkFile = File(context.cacheDir, "update.apk") - Log.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}") - - FileOutputStream(apkFile).use { outputStream -> - connection.inputStream.use { inputStream -> - inputStream.copyTo(outputStream) - } - } - Log.i(AppConfig.TAG, "APK download completed") - return@withContext apkFile - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to download APK: ${e.message}") - return@withContext null - } finally { - try { - connection.disconnect() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Error closing connection: ${e.message}") - } - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to initiate download: ${e.message}") - return@withContext null - } - } - - private fun compareVersions(version1: String, version2: String): Int { - val v1 = version1.split(".") - val v2 = version2.split(".") - - for (i in 0 until maxOf(v1.size, v2.size)) { - val num1 = if (i < v1.size) v1[i].toInt() else 0 - val num2 = if (i < v2.size) v2[i].toInt() else 0 - if (num1 != num2) return num1 - num2 - } - return 0 - } - - private fun getDownloadUrl(release: GitHubRelease, abi: String): String { - return release.assets.find { it.name.contains(abi) }?.browserDownloadUrl - ?: release.assets.firstOrNull()?.browserDownloadUrl - ?: throw IllegalStateException("No compatible APK found") - } -} 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 deleted file mode 100644 index f53697bb..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt +++ /dev/null @@ -1,1091 +0,0 @@ -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.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.OutboundBean -import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean -import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean -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.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.HttpUtil -import com.v2ray.ang.util.JsonUtil -import com.v2ray.ang.util.Utils - -object V2rayConfigManager { - private var initConfigCache: String? = null - - //region get config function - - /** - * Retrieves the V2ray configuration for the given GUID. - * - * @param context The context of the caller. - * @param guid The unique identifier for the V2ray configuration. - * @return A ConfigResult object containing the configuration details or indicating failure. - */ - fun getV2rayConfig(context: Context, guid: String): ConfigResult { - try { - val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false) - return if (config.configType == EConfigType.CUSTOM) { - getV2rayCustomConfig(guid, config) - } else { - getV2rayNormalConfig(context, guid, config) - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to get V2ray config", e) - return ConfigResult(false) - } - } - - /** - * Retrieves the speedtest V2ray configuration for the given GUID. - * - * @param context The context of the caller. - * @param guid The unique identifier for the V2ray configuration. - * @return A ConfigResult object containing the configuration details or indicating failure. - */ - fun getV2rayConfig4Speedtest(context: Context, guid: String): ConfigResult { - try { - val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false) - return if (config.configType == EConfigType.CUSTOM) { - getV2rayCustomConfig(guid, config) - } else { - getV2rayNormalConfig4Speedtest(context, guid, config) - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to get V2ray config for speedtest", e) - return ConfigResult(false) - } - } - - /** - * Retrieves the custom V2ray configuration. - * - * @param guid The unique identifier for the V2ray configuration. - * @param config The profile item containing the configuration details. - * @return A ConfigResult object containing the result of the configuration retrieval. - */ - private fun getV2rayCustomConfig(guid: String, config: ProfileItem): ConfigResult { - val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false) - return ConfigResult(true, guid, raw) - } - - /** - * Retrieves the normal V2ray configuration. - * - * @param context The context in which the function is called. - * @param guid The unique identifier for the V2ray configuration. - * @param config The profile item containing the configuration details. - * @return A ConfigResult object containing the result of the configuration retrieval. - */ - private fun getV2rayNormalConfig(context: Context, guid: String, config: ProfileItem): ConfigResult { - val result = ConfigResult(false) - - val address = config.server ?: return result - if (!Utils.isPureIpAddress(address)) { - if (!Utils.isValidUrl(address)) { - Log.w(AppConfig.TAG, "$address is an invalid ip or domain") - return result - } - } - - val v2rayConfig = initV2rayConfig(context) ?: return result - v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning" - v2rayConfig.remarks = config.remarks - - getInbounds(v2rayConfig) - - if (config.configType == EConfigType.HYSTERIA2) { - result.socksPort = getPlusOutbounds(v2rayConfig, config) ?: return result - } else { - getOutbounds(v2rayConfig, config) ?: return result - getMoreOutbounds(v2rayConfig, config.subscriptionId) - } - - getRouting(v2rayConfig) - - getFakeDns(v2rayConfig) - - getDns(v2rayConfig) - - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { - getCustomLocalDns(v2rayConfig) - } - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) { - v2rayConfig.stats = null - v2rayConfig.policy = null - } - - //Resolve and add to DNS Hosts - if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") == "1") { - resolveOutboundDomainsToHosts(v2rayConfig) - } - - result.status = true - result.content = JsonUtil.toJsonPretty(v2rayConfig) ?: "" - result.guid = guid - return result - } - - /** - * Retrieves the normal V2ray configuration for speedtest. - * - * @param context The context in which the function is called. - * @param guid The unique identifier for the V2ray configuration. - * @param config The profile item containing the configuration details. - * @return A ConfigResult object containing the result of the configuration retrieval. - */ - private fun getV2rayNormalConfig4Speedtest(context: Context, guid: String, config: ProfileItem): ConfigResult { - val result = ConfigResult(false) - - val address = config.server ?: return result - if (!Utils.isPureIpAddress(address)) { - if (!Utils.isValidUrl(address)) { - Log.w(AppConfig.TAG, "$address is an invalid ip or domain") - return result - } - } - - val v2rayConfig = initV2rayConfig(context) ?: return result - - if (config.configType == EConfigType.HYSTERIA2) { - result.socksPort = getPlusOutbounds(v2rayConfig, config) ?: return result - } else { - getOutbounds(v2rayConfig, config) ?: return result - getMoreOutbounds(v2rayConfig, config.subscriptionId) - } - - v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning" - v2rayConfig.inbounds.clear() - v2rayConfig.routing.rules.clear() - v2rayConfig.dns = null - v2rayConfig.fakedns = null - v2rayConfig.stats = null - v2rayConfig.policy = null - - v2rayConfig.outbounds.forEach { key -> - key.mux = null - } - - result.status = true - result.content = JsonUtil.toJsonPretty(v2rayConfig) ?: "" - result.guid = guid - return result - } - - /** - * Initializes V2ray configuration. - * - * This function loads the V2ray configuration from assets or from a cached value. - * It first attempts to use the cached configuration if available, otherwise reads - * the configuration from the "v2ray_config.json" asset file. - * - * @param context Android context used to access application assets - * @return V2rayConfig object parsed from the JSON configuration, or null if the configuration is empty - */ - private fun initV2rayConfig(context: Context): V2rayConfig? { - val assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json") - if (TextUtils.isEmpty(assets)) { - return null - } - initConfigCache = assets - val config = JsonUtil.fromJson(assets, V2rayConfig::class.java) - return config - } - - - //endregion - - - //region some sub function - - /** - * Configures the inbound settings for V2ray. - * - * This function sets up the listening ports, sniffing options, and other inbound-related configurations. - * - * @param v2rayConfig The V2ray configuration object to be modified - * @return true if inbound configuration was successful, false otherwise - */ - private fun getInbounds(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 = AppConfig.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) { - Log.e(AppConfig.TAG, "Failed to configure inbounds", e) - return false - } - return true - } - - /** - * Configures the fake DNS settings if enabled. - * - * Adds FakeDNS configuration to v2rayConfig if both local DNS and fake DNS are enabled. - * - * @param v2rayConfig The V2ray configuration object to be modified - */ - private fun getFakeDns(v2rayConfig: V2rayConfig) { - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true - && MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true - ) { - v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean()) - } - } - - /** - * Configures routing settings for V2ray. - * - * Sets up the domain strategy and adds routing rules from saved rulesets. - * - * @param v2rayConfig The V2ray configuration object to be modified - * @return true if routing configuration was successful, false otherwise - */ - private fun getRouting(v2rayConfig: V2rayConfig): Boolean { - try { - - v2rayConfig.routing.domainStrategy = - MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) - ?: "AsIs" - - val rulesetItems = MmkvManager.decodeRoutingRulesets() - rulesetItems?.forEach { key -> - getRoutingUserRule(key, v2rayConfig) - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to configure routing", e) - return false - } - return true - } - - /** - * Adds a specific ruleset item to the routing configuration. - * - * @param item The ruleset item to add - * @param v2rayConfig The V2ray configuration object to be modified - */ - private fun getRoutingUserRule(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) { - Log.e(AppConfig.TAG, "Failed to apply routing user rule", e) - } - } - - /** - * Retrieves domain rules for a specific outbound tag. - * - * Searches through all rulesets to find domains targeting the specified tag. - * - * @param tag The outbound tag to search for - * @return ArrayList of domain rules matching the tag - */ - private fun getUserRule2Domain(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 != AppConfig.GEOSITE_PRIVATE - && (it.startsWith("geosite:") || it.startsWith("domain:")) - ) { - domain.add(it) - } - } - } - } - - return domain - } - - /** - * Configures custom local DNS settings. - * - * Sets up DNS inbound, outbound, and routing rules for local DNS resolution. - * - * @param v2rayConfig The V2ray configuration object to be modified - * @return true if custom local DNS configuration was successful, false otherwise - */ - private fun getCustomLocalDns(v2rayConfig: V2rayConfig): Boolean { - try { - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) { - val geositeCn = arrayListOf(AppConfig.GEOSITE_CN) - val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY) - val directDomain = getUserRule2Domain(AppConfig.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 = SettingsManager.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 = AppConfig.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) { - Log.e(AppConfig.TAG, "Failed to configure custom local DNS", e) - return false - } - return true - } - - /** - * Configures the DNS settings for V2ray. - * - * Sets up DNS servers, hosts, and routing rules for DNS resolution. - * - * @param v2rayConfig The V2ray configuration object to be modified - * @return true if DNS configuration was successful, false otherwise - */ - private fun getDns(v2rayConfig: V2rayConfig): Boolean { - try { - val hosts = mutableMapOf() - val servers = ArrayList() - - //remote Dns - val remoteDns = SettingsManager.getRemoteDnsServers() - val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY) - remoteDns.forEach { - servers.add(it) - } - if (proxyDomain.isNotEmpty()) { - servers.add( - V2rayConfig.DnsBean.ServersBean( - address = remoteDns.first(), - domains = proxyDomain, - ) - ) - } - - // domestic DNS - val domesticDns = SettingsManager.getDomesticDnsServers() - val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT) - val isCnRoutingMode = directDomain.contains(AppConfig.GEOSITE_CN) - val geoipCn = arrayListOf(AppConfig.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 = AppConfig.TAG_DIRECT, - port = "53", - ip = arrayListOf(domesticDns.first()), - domain = null - ) - ) - } - - //block dns - val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED) - if (blkDomain.isNotEmpty()) { - hosts.putAll(blkDomain.map { it to AppConfig.LOOPBACK }) - } - - // hardcode googleapi rule to fix play store problems - hosts[AppConfig.GOOGLEAPIS_CN_DOMAIN] = AppConfig.GOOGLEAPIS_COM_DOMAIN - - // hardcode popular Android Private DNS rule to fix localhost DNS problem - hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES - hosts[AppConfig.DNS_CLOUDFLARE_ONE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ONE_ADDRESSES - hosts[AppConfig.DNS_CLOUDFLARE_DNS_COM_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_COM_ADDRESSES - hosts[AppConfig.DNS_CLOUDFLARE_DNS_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_ADDRESSES - hosts[AppConfig.DNS_DNSPOD_DOMAIN] = AppConfig.DNS_DNSPOD_ADDRESSES - hosts[AppConfig.DNS_GOOGLE_DOMAIN] = AppConfig.DNS_GOOGLE_ADDRESSES - hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES - hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES - - //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) { - Log.e(AppConfig.TAG, "Failed to configure user DNS hosts", e) - } - - // DNS dns - v2rayConfig.dns = V2rayConfig.DnsBean( - servers = servers, - hosts = hosts - ) - - // DNS routing - if (Utils.isPureIpAddress(remoteDns.first())) { - v2rayConfig.routing.rules.add( - 0, RulesBean( - outboundTag = AppConfig.TAG_PROXY, - port = "53", - ip = arrayListOf(remoteDns.first()), - domain = null - ) - ) - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to configure DNS", e) - return false - } - return true - } - - - //endregion - - - //region outbound related functions - - /** - * Configures the primary outbound connection. - * - * Converts the profile to an outbound configuration and applies global settings. - * - * @param v2rayConfig The V2ray configuration object to be modified - * @param config The profile item containing connection details - * @return true if outbound configuration was successful, null if there was an error - */ - private fun getOutbounds(v2rayConfig: V2rayConfig, config: ProfileItem): Boolean? { - val outbound = convertProfile2Outbound(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 true - } - - /** - * Configures special outbound settings for Hysteria2 protocol. - * - * Creates a SOCKS outbound connection on a free port for protocols requiring special handling. - * - * @param v2rayConfig The V2ray configuration object to be modified - * @param config The profile item containing connection details - * @return The port number for the SOCKS connection, or null if there was an error - */ - private fun getPlusOutbounds(v2rayConfig: V2rayConfig, config: ProfileItem): Int? { - try { - val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0)) - - val outboundNew = OutboundBean( - mux = null, - protocol = EConfigType.SOCKS.name.lowercase(), - settings = OutSettingsBean( - servers = listOf( - OutSettingsBean.ServersBean( - address = AppConfig.LOOPBACK, - port = socksPort - ) - ) - ) - ) - if (v2rayConfig.outbounds.isNotEmpty()) { - v2rayConfig.outbounds[0] = outboundNew - } else { - v2rayConfig.outbounds.add(outboundNew) - } - - return socksPort - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to configure plusOutbound", e) - return null - } - } - - /** - * Configures additional outbound connections for proxy chaining. - * - * Sets up previous and next proxies in a subscription for advanced routing capabilities. - * - * @param v2rayConfig The V2ray configuration object to be modified - * @param subscriptionId The subscription ID to look up related proxies - * @return true if additional outbounds were configured successfully, false otherwise - */ - private fun getMoreOutbounds(v2rayConfig: V2rayConfig, subscriptionId: String): Boolean { - //fragment proxy - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) { - return false - } - - if (subscriptionId.isEmpty()) { - return false - } - try { - val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return false - - //current proxy - val outbound = v2rayConfig.outbounds[0] - - //Previous proxy - val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile) - if (prevNode != null) { - val prevOutbound = convertProfile2Outbound(prevNode) - if (prevOutbound != null) { - updateOutboundWithGlobalSettings(prevOutbound) - prevOutbound.tag = AppConfig.TAG_PROXY + "2" - v2rayConfig.outbounds.add(prevOutbound) - outbound.ensureSockopt().dialerProxy = prevOutbound.tag - } - } - - //Next proxy - val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile) - if (nextNode != null) { - val nextOutbound = convertProfile2Outbound(nextNode) - if (nextOutbound != null) { - updateOutboundWithGlobalSettings(nextOutbound) - nextOutbound.tag = AppConfig.TAG_PROXY - v2rayConfig.outbounds.add(0, nextOutbound) - outbound.tag = AppConfig.TAG_PROXY + "1" - nextOutbound.ensureSockopt().dialerProxy = outbound.tag - } - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to configure more outbounds", e) - return false - } - - return true - } - - /** - * Updates outbound settings based on global preferences. - * - * Applies multiplexing and protocol-specific settings to an outbound connection. - * - * @param outbound The outbound connection to update - * @return true if the update was successful, false otherwise - */ - 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(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4) - } 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 == AppConfig.DEFAULT_NETWORK - && outbound.streamSettings?.tcpSettings?.header?.type == AppConfig.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, - 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) { - Log.e(AppConfig.TAG, "Failed to update outbound with global settings", e) - return false - } - return true - } - - /** - * Updates the outbound with fragment settings for traffic optimization. - * - * Configures packet fragmentation for TLS and REALITY protocols if enabled. - * - * @param v2rayConfig The V2ray configuration object to be modified - * @return true if fragment configuration was successful, false otherwise - */ - private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean { - try { - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) { - return true - } - if (v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.TLS - && v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.REALITY - ) { - return true - } - - val fragmentOutbound = - V2rayConfig.OutboundBean( - protocol = AppConfig.PROTOCOL_FREEDOM, - tag = AppConfig.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 = "tlshello" - } - - fragmentOutbound.settings = OutboundBean.OutSettingsBean( - fragment = OutboundBean.OutSettingsBean.FragmentBean( - packets = packets, - length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH) - ?: "50-100", - interval = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL) - ?: "10-20" - ), - noises = listOf( - OutboundBean.OutSettingsBean.NoiseBean( - type = "rand", - packet = "10-20", - delay = "10-16", - ) - ), - ) - fragmentOutbound.streamSettings = StreamSettingsBean( - sockopt = StreamSettingsBean.SockoptBean( - TcpNoDelay = true, - mark = 255 - ) - ) - v2rayConfig.outbounds.add(fragmentOutbound) - - //proxy chain - v2rayConfig.outbounds[0].streamSettings?.sockopt = - StreamSettingsBean.SockoptBean( - dialerProxy = AppConfig.TAG_FRAGMENT - ) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to update outbound fragment", e) - return false - } - return true - } - - /** - * Resolves domain names to IP addresses in outbound connections. - * - * Pre-resolves domains to improve connection speed and reliability. - * - * @param v2rayConfig The V2ray configuration object to be modified - */ - private fun resolveOutboundDomainsToHosts(v2rayConfig: V2rayConfig) { - val proxyOutboundList = v2rayConfig.getAllProxyOutbound() - val dns = v2rayConfig.dns ?: return - val newHosts = dns.hosts?.toMutableMap() ?: mutableMapOf() - val preferIpv6 = MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true - - for (item in proxyOutboundList) { - val domain = item.getServerAddress() - if (domain.isNullOrEmpty()) continue - - if (newHosts.containsKey(domain)) { - item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6" - continue - } - - val resolvedIps = HttpUtil.resolveHostToIP(domain, preferIpv6) - if (resolvedIps.isNullOrEmpty()) continue - - item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6" - newHosts[domain] = if (resolvedIps.size == 1) { - resolvedIps[0] - } else { - resolvedIps - } - } - - dns.hosts = newHosts - } - - /** - * Converts a profile item to an outbound configuration. - * - * Creates appropriate outbound settings based on the protocol type. - * - * @param profileItem The profile item to convert - * @return OutboundBean configuration for the profile, or null if not supported - */ - private fun convertProfile2Outbound(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 -> null - EConfigType.HTTP -> HttpFmt.toOutbound(profileItem) - } - } - - /** - * Creates an initial outbound configuration for a specific protocol type. - * - * Provides a template configuration for different protocol types. - * - * @param configType The type of configuration to create - * @return An initial OutboundBean for the specified configuration type, or null for custom types - */ - fun createInitOutbound(configType: EConfigType): OutboundBean? { - return when (configType) { - EConfigType.VMESS, - EConfigType.VLESS -> - return OutboundBean( - protocol = configType.name.lowercase(), - settings = OutSettingsBean( - vnext = listOf( - OutSettingsBean.VnextBean( - users = listOf(OutSettingsBean.VnextBean.UsersBean()) - ) - ) - ), - streamSettings = StreamSettingsBean() - ) - - EConfigType.SHADOWSOCKS, - EConfigType.SOCKS, - EConfigType.HTTP, - EConfigType.TROJAN, - EConfigType.HYSTERIA2 -> - return OutboundBean( - protocol = configType.name.lowercase(), - settings = OutSettingsBean( - servers = listOf(OutSettingsBean.ServersBean()) - ), - streamSettings = StreamSettingsBean() - ) - - EConfigType.WIREGUARD -> - return OutboundBean( - protocol = configType.name.lowercase(), - settings = OutSettingsBean( - secretKey = "", - peers = listOf(OutSettingsBean.WireGuardBean()) - ) - ) - - EConfigType.CUSTOM -> null - } - } - - /** - * Configures transport settings for an outbound connection. - * - * Sets up protocol-specific transport options based on the profile settings. - * - * @param streamSettings The stream settings to configure - * @param profileItem The profile containing transport configuration - * @return The Server Name Indication (SNI) value to use, or null if not applicable - */ - fun populateTransportSettings(streamSettings: StreamSettingsBean, profileItem: ProfileItem): String? { - val transport = profileItem.network.orEmpty() - val headerType = profileItem.headerType - val host = profileItem.host - val path = profileItem.path - val seed = profileItem.seed -// val quicSecurity = profileItem.quicSecurity -// val key = profileItem.quicKey - val mode = profileItem.mode - val serviceName = profileItem.serviceName - val authority = profileItem.authority - val xhttpMode = profileItem.xhttpMode - val xhttpExtra = profileItem.xhttpExtra - - var sni: String? = null - streamSettings.network = if (transport.isEmpty()) NetworkType.TCP.type else transport - when (streamSettings.network) { - NetworkType.TCP.type -> { - val tcpSetting = StreamSettingsBean.TcpSettingsBean() - if (headerType == AppConfig.HEADER_TYPE_HTTP) { - tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP - if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) { - val requestObj = StreamSettingsBean.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() } - tcpSetting.header.request = requestObj - sni = requestObj.headers.Host?.getOrNull(0) - } - } else { - tcpSetting.header.type = "none" - sni = host - } - streamSettings.tcpSettings = tcpSetting - } - - NetworkType.KCP.type -> { - val kcpsetting = StreamSettingsBean.KcpSettingsBean() - kcpsetting.header.type = headerType ?: "none" - if (seed.isNullOrEmpty()) { - kcpsetting.seed = null - } else { - kcpsetting.seed = seed - } - if (host.isNullOrEmpty()) { - kcpsetting.header.domain = null - } else { - kcpsetting.header.domain = host - } - streamSettings.kcpSettings = kcpsetting - } - - NetworkType.WS.type -> { - val wssetting = StreamSettingsBean.WsSettingsBean() - wssetting.headers.Host = host.orEmpty() - sni = host - wssetting.path = path ?: "/" - streamSettings.wsSettings = wssetting - } - - NetworkType.HTTP_UPGRADE.type -> { - val httpupgradeSetting = StreamSettingsBean.HttpupgradeSettingsBean() - httpupgradeSetting.host = host.orEmpty() - sni = host - httpupgradeSetting.path = path ?: "/" - streamSettings.httpupgradeSettings = httpupgradeSetting - } - - NetworkType.XHTTP.type -> { - val xhttpSetting = StreamSettingsBean.XhttpSettingsBean() - xhttpSetting.host = host.orEmpty() - sni = host - xhttpSetting.path = path ?: "/" - xhttpSetting.mode = xhttpMode - xhttpSetting.extra = JsonUtil.parseString(xhttpExtra) - streamSettings.xhttpSettings = xhttpSetting - } - - NetworkType.H2.type, NetworkType.HTTP.type -> { - streamSettings.network = NetworkType.H2.type - val h2Setting = StreamSettingsBean.HttpSettingsBean() - h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() } - sni = h2Setting.host.getOrNull(0) - h2Setting.path = path ?: "/" - streamSettings.httpSettings = h2Setting - } - -// "quic" -> { -// val quicsetting = QuicSettingBean() -// quicsetting.security = quicSecurity ?: "none" -// quicsetting.key = key.orEmpty() -// quicsetting.header.type = headerType ?: "none" -// quicSettings = quicsetting -// } - - NetworkType.GRPC.type -> { - val grpcSetting = StreamSettingsBean.GrpcSettingsBean() - grpcSetting.multiMode = mode == "multi" - grpcSetting.serviceName = serviceName.orEmpty() - grpcSetting.authority = authority.orEmpty() - grpcSetting.idle_timeout = 60 - grpcSetting.health_check_timeout = 20 - sni = authority - streamSettings.grpcSettings = grpcSetting - } - } - return sni - } - - /** - * Configures TLS or REALITY security settings for an outbound connection. - * - * Sets up security-related parameters like certificates, fingerprints, and SNI. - * - * @param streamSettings The stream settings to configure - * @param profileItem The profile containing security configuration - * @param sniExt An external SNI value to use if the profile doesn't specify one - */ - fun populateTlsSettings(streamSettings: StreamSettingsBean, profileItem: ProfileItem, sniExt: String?) { - val streamSecurity = profileItem.security.orEmpty() - val allowInsecure = profileItem.insecure == true - val sni = if (profileItem.sni.isNullOrEmpty()) { - when { - sniExt.isNotNullEmpty() && Utils.isDomainName(sniExt) -> sniExt - profileItem.server.isNotNullEmpty() && Utils.isDomainName(profileItem.server) -> profileItem.server - else -> sniExt - } - } else { - profileItem.sni - } - val fingerprint = profileItem.fingerPrint - val alpns = profileItem.alpn - val publicKey = profileItem.publicKey - val shortId = profileItem.shortId - val spiderX = profileItem.spiderX - - streamSettings.security = if (streamSecurity.isEmpty()) null else streamSecurity - if (streamSettings.security == null) return - val tlsSetting = StreamSettingsBean.TlsSettingsBean( - allowInsecure = allowInsecure, - 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 = if (publicKey.isNullOrEmpty()) null else publicKey, - shortId = if (shortId.isNullOrEmpty()) null else shortId, - spiderX = if (spiderX.isNullOrEmpty()) null else spiderX, - ) - if (streamSettings.security == AppConfig.TLS) { - streamSettings.tlsSettings = tlsSetting - streamSettings.realitySettings = null - } else if (streamSettings.security == AppConfig.REALITY) { - streamSettings.tlsSettings = null - streamSettings.realitySettings = tlsSetting - } - } - - //endregion -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/CustomDividerItemDecoration.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/CustomDividerItemDecoration.kt deleted file mode 100644 index 0f6d37c0..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/CustomDividerItemDecoration.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.v2ray.ang.helper - -import android.graphics.Canvas -import android.graphics.Rect -import android.graphics.drawable.Drawable -import android.view.View -import androidx.recyclerview.widget.RecyclerView - -class CustomDividerItemDecoration( - private val divider: Drawable, - private val orientation: Int -) : RecyclerView.ItemDecoration() { - - override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { - if (orientation == RecyclerView.VERTICAL) { - drawVerticalDividers(canvas, parent) - } else { - drawHorizontalDividers(canvas, parent) - } - } - - private fun drawVerticalDividers(canvas: Canvas, parent: RecyclerView) { - val left = parent.paddingLeft - val right = parent.width - parent.paddingRight - - val childCount = parent.childCount - for (i in 0 until childCount - 1) { - val child = parent.getChildAt(i) - val params = child.layoutParams as RecyclerView.LayoutParams - - val top = child.bottom + params.bottomMargin - val bottom = top + divider.intrinsicHeight - - divider.setBounds(left, top, right, bottom) - divider.draw(canvas) - } - } - - private fun drawHorizontalDividers(canvas: Canvas, parent: RecyclerView) { - val top = parent.paddingTop - val bottom = parent.height - parent.paddingBottom - - val childCount = parent.childCount - for (i in 0 until childCount - 1) { - val child = parent.getChildAt(i) - val params = child.layoutParams as RecyclerView.LayoutParams - - val left = child.right + params.rightMargin - val right = left + divider.intrinsicWidth - - divider.setBounds(left, top, right, bottom) - divider.draw(canvas) - } - } - - override fun getItemOffsets( - outRect: Rect, - view: View, - parent: RecyclerView, - state: RecyclerView.State - ) { - if (orientation == RecyclerView.VERTICAL) { - outRect.set(0, 0, 0, divider.intrinsicHeight) - } else { - outRect.set(0, 0, divider.intrinsicWidth, 0) - } - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java similarity index 53% rename from V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java index 8707d16f..2de0c74a 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java @@ -13,41 +13,48 @@ * 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.RecyclerView; +import androidx.recyclerview.widget.ItemTouchHelper; /** - * Interface to listen for a move or dismissal event from a [ItemTouchHelper.Callback]. + * Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}. * * @author Paul Burke (ipaulpro) */ -interface ItemTouchHelperAdapter { +public 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 [RecyclerView.Adapter.notifyItemMoved] after + * an item is shifted, and not at the end of a "drop" event.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} 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 - * @see RecyclerView.ViewHolder.getAdapterPosition + * + * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) + * @see RecyclerView.ViewHolder#getAdapterPosition() */ - fun onItemMove(fromPosition: Int, toPosition: Int): Boolean + boolean onItemMove(int fromPosition, int toPosition); - fun onItemMoveCompleted() + void onItemMoveCompleted(); /** - * Called when an item has been dismissed by a swipe.

- *

- * Implementations should call [RecyclerView.Adapter.notifyItemRemoved] after + * Called when an item has been dismissed by a swipe.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after * adjusting the underlying data to reflect this removal. * * @param position The position of the item dismissed. - * @see RecyclerView.getAdapterPositionFor - * @see RecyclerView.ViewHolder.getAdapterPosition + * + * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) + * @see RecyclerView.ViewHolder#getAdapterPosition() */ - fun onItemDismiss(position: Int) + void onItemDismiss(int position); } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java similarity index 67% rename from V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java index 75655b0c..149768fc 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java @@ -13,26 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.v2ray.ang.helper -import androidx.recyclerview.widget.ItemTouchHelper +package com.v2ray.ang.helper; + +import androidx.recyclerview.widget.ItemTouchHelper; /** - * Interface to notify an item ViewHolder of relevant callbacks from [ ]. + * Interface to notify an item ViewHolder of relevant callbacks from {@link + * ItemTouchHelper.Callback}. * * @author Paul Burke (ipaulpro) */ -interface ItemTouchHelperViewHolder { +public interface ItemTouchHelperViewHolder { + /** - * Called when the [ItemTouchHelper] first registers an item as being moved or swiped. + * Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped. * Implementations should update the item view to indicate it's active state. */ - fun onItemSelected() + void onItemSelected(); /** - * Called when the [ItemTouchHelper] has completed the move or swipe, and the active item + * Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item * state should be cleared. */ - fun onItemClear() + void 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 new file mode 100644 index 00000000..a6407b6f --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java @@ -0,0 +1,33 @@ +/* + * 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 new file mode 100644 index 00000000..97fceab7 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java @@ -0,0 +1,128 @@ +/* + * 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.RecyclerView; +import androidx.recyclerview.widget.ItemTouchHelper; + +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 deleted file mode 100644 index b98129a7..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (C) 2015 Paul Burke - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.v2ray.ang.helper - -import android.animation.ValueAnimator -import android.graphics.Canvas -import android.view.animation.DecelerateInterpolator -import androidx.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 - } -} 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 deleted file mode 100644 index 7a072cd0..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/NativePlugin.kt +++ /dev/null @@ -1,32 +0,0 @@ -/****************************************************************************** - * * - * 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 deleted file mode 100644 index 04294ac6..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/Plugin.kt +++ /dev/null @@ -1,43 +0,0 @@ -/****************************************************************************** - * * - * 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 deleted file mode 100644 index 5aa253f8..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginContract.kt +++ /dev/null @@ -1,33 +0,0 @@ -/****************************************************************************** - * * - * 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 deleted file mode 100644 index 2495eb45..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginList.kt +++ /dev/null @@ -1,54 +0,0 @@ -/****************************************************************************** - * * - * 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 deleted file mode 100644 index e7e8457a..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginManager.kt +++ /dev/null @@ -1,233 +0,0 @@ -/****************************************************************************** - * * - * 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 androidx.core.os.bundleOf -import com.v2ray.ang.AngApplication -import com.v2ray.ang.extension.listenForPackageChanges -import com.v2ray.ang.extension.toast -import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID -import java.io.File -import java.io.FileNotFoundException - -object PluginManager { - - class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin) - - private var receiver: BroadcastReceiver? = null - private var cachedPlugins: PluginList? = null - fun fetchPlugins() = synchronized(this) { - if (receiver == null) receiver = AngApplication.application.listenForPackageChanges { - synchronized(this) { - receiver = null - cachedPlugins = null - } - } - if (cachedPlugins == null) cachedPlugins = PluginList() - cachedPlugins!! - } - - private fun buildUri(id: String, authority: String) = Uri.Builder() - .scheme(PluginContract.SCHEME) - .authority(authority) - .path("/$id") - .build() - - data class InitResult( - val path: String, - ) - - @Throws(Throwable::class) - fun init(pluginId: String): InitResult? { - if (pluginId.isEmpty()) return null - var throwable: Throwable? = null - - try { - val result = initNative(pluginId) - if (result != null) return result - } catch (t: Throwable) { - if (throwable == null) throwable = t //Logs.w(t) - } - - throw throwable ?: PluginNotFoundException(pluginId) - } - - private fun initNative(pluginId: String): InitResult? { - var flags = PackageManager.GET_META_DATA - if (Build.VERSION.SDK_INT >= 24) { - flags = - flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE - } - var providers = AngApplication.application.packageManager.queryIntentContentProviders( - Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "com.github.dyhkwong.AngApplication")), flags - ) - .filter { it.providerInfo.exported } - if (providers.isEmpty()) { - providers = AngApplication.application.packageManager.queryIntentContentProviders( - Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "io.nekohasekai.AngApplication")), flags - ) - .filter { it.providerInfo.exported } - } - if (providers.isEmpty()) { - providers = AngApplication.application.packageManager.queryIntentContentProviders( - Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "moe.matsuri.lite")), flags - ) - .filter { it.providerInfo.exported } - } - if (providers.isEmpty()) { - providers = AngApplication.application.packageManager.queryIntentContentProviders( - Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "fr.husi")), flags - ) - .filter { it.providerInfo.exported } - } - if (providers.isEmpty()) { - providers = AngApplication.application.packageManager.queryIntentContentProviders( - Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA - ).filter { - it.providerInfo.exported && - it.providerInfo.metaData.containsKey(METADATA_KEY_ID) && - it.providerInfo.metaData.getString(METADATA_KEY_ID) == pluginId - } - if (providers.size > 1) { - providers = listOf(providers[0]) // What if there is more than one? - } - } - if (providers.isEmpty()) return null - if (providers.size > 1) { - val message = - "Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}" - AngApplication.application.toast(message) - throw IllegalStateException(message) - } - val provider = providers.single().providerInfo - var failure: Throwable? = null - try { - initNativeFaster(provider)?.also { return InitResult(it) } - } catch (t: Throwable) { - // Logs.w("Initializing native plugin faster mode failed") - failure = t - } - - val uri = Uri.Builder().apply { - scheme(ContentResolver.SCHEME_CONTENT) - authority(provider.authority) - }.build() - try { - return initNativeFast( - AngApplication.application.contentResolver, - pluginId, - uri - )?.let { InitResult(it) } - } catch (t: Throwable) { - // Logs.w("Initializing native plugin fast mode failed") - failure?.also { t.addSuppressed(it) } - failure = t - } - - try { - return initNativeSlow( - AngApplication.application.contentResolver, - pluginId, - uri - )?.let { InitResult(it) } - } catch (t: Throwable) { - failure?.also { t.addSuppressed(it) } - throw t - } - } - - private fun initNativeFaster(provider: ProviderInfo): String? { - return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH) - ?.let { relativePath -> - File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply { - check(canExecute()) - }.absolutePath - } - } - - private fun initNativeFast(cr: ContentResolver, pluginId: String, uri: Uri): String? { - return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, bundleOf()) - ?.getString(PluginContract.EXTRA_ENTRY)?.also { - check(File(it).canExecute()) - } - } - - @SuppressLint("Recycle") - private fun initNativeSlow(cr: ContentResolver, pluginId: String, uri: Uri): String? { - var initialized = false - fun entryNotFound(): Nothing = - throw IndexOutOfBoundsException("Plugin entry binary not found") - - val pluginDir = File(AngApplication.application.noBackupFilesDir, "plugin") - (cr.query( - uri, - arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), - null, - null, - null - ) - ?: return null).use { cursor -> - if (!cursor.moveToFirst()) entryNotFound() - pluginDir.deleteRecursively() - if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory") - val pluginDirPath = pluginDir.absolutePath + '/' - do { - val path = cursor.getString(0) - val file = File(pluginDir, path) - check(file.absolutePath.startsWith(pluginDirPath)) - cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream -> - file.outputStream().use { outStream -> inStream.copyTo(outStream) } - } - Os.chmod( - file.absolutePath, when (cursor.getType(1)) { - Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1) - Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8) - else -> throw IllegalArgumentException("File mode should be of type int") - } - ) - if (path == pluginId) initialized = true - } while (cursor.moveToNext()) - } - if (!initialized) entryNotFound() - return File(pluginDir, pluginId).absolutePath - } - - fun ComponentInfo.loadString(key: String) = when (val value = metaData.getString(key)) { - is String -> value -// is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo) -// .getString(value) - - null -> null - 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 deleted file mode 100644 index 6e6861cf..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/ResolvedPlugin.kt +++ /dev/null @@ -1,51 +0,0 @@ -/****************************************************************************** - * * - * 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 deleted file mode 100644 index ae15e76c..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt +++ /dev/null @@ -1,23 +0,0 @@ -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() { - /** - * This method is called when the BroadcastReceiver is receiving an Intent broadcast. - * It checks if the context is not null and the action is ACTION_BOOT_COMPLETED. - * If the conditions are met, it starts the V2Ray service. - * - * @param context The Context in which the receiver is running. - * @param intent The Intent being received. - */ - override fun onReceive(context: Context?, intent: Intent?) { - if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return - if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return - V2RayServiceManager.startVService(context) - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt deleted file mode 100644 index 92c551a6..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt +++ /dev/null @@ -1,252 +0,0 @@ -package com.v2ray.ang.service - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.app.Service -import android.content.Context -import android.content.Intent -import android.graphics.Color -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import com.v2ray.ang.AppConfig -import com.v2ray.ang.AppConfig.ANG_PACKAGE -import com.v2ray.ang.AppConfig.TAG_DIRECT -import com.v2ray.ang.R -import com.v2ray.ang.dto.ProfileItem -import com.v2ray.ang.extension.toSpeedString -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.ui.MainActivity -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlin.math.min - -object NotificationService { - 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 - - private var lastQueryTime = 0L - private var mBuilder: NotificationCompat.Builder? = null - private var speedNotificationJob: Job? = null - private var mNotificationManager: NotificationManager? = null - - /** - * Starts the speed notification. - * @param currentConfig The current profile configuration. - */ - fun startSpeedNotification(currentConfig: ProfileItem?) { - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) return - if (speedNotificationJob != null || V2RayServiceManager.isRunning() == false) return - - lastQueryTime = System.currentTimeMillis() - var lastZeroSpeed = false - val outboundTags = currentConfig?.getAllOutboundTags() - outboundTags?.remove(TAG_DIRECT) - - speedNotificationJob = CoroutineScope(Dispatchers.IO).launch { - while (isActive) { - val queryTime = System.currentTimeMillis() - val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0 - var proxyTotal = 0L - val text = StringBuilder() - outboundTags?.forEach { - val up = V2RayServiceManager.queryStats(it, AppConfig.UPLINK) - val down = V2RayServiceManager.queryStats(it, AppConfig.DOWNLINK) - if (up + down > 0) { - appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds) - proxyTotal += up + down - } - } - val directUplink = V2RayServiceManager.queryStats(TAG_DIRECT, AppConfig.UPLINK) - val directDownlink = V2RayServiceManager.queryStats(TAG_DIRECT, AppConfig.DOWNLINK) - val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L - if (!zeroSpeed || !lastZeroSpeed) { - if (proxyTotal == 0L) { - appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0) - } - appendSpeedString( - text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds, - directDownlink / sinceLastQueryInSeconds - ) - updateNotification(text.toString(), proxyTotal, directDownlink + directUplink) - } - lastZeroSpeed = zeroSpeed - lastQueryTime = queryTime - delay(3000) - } - } - } - - /** - * Shows the notification. - * @param currentConfig The current profile configuration. - */ - fun showNotification(currentConfig: ProfileItem?) { - val service = 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, 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 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) { - createNotificationChannel() - } else { - // If earlier version channel ID is not used - // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) - "" - } - - mBuilder = NotificationCompat.Builder(service, channelId) - .setSmallIcon(R.drawable.ic_stat_name) - .setContentTitle(currentConfig?.remarks) - .setPriority(NotificationCompat.PRIORITY_MIN) - .setOngoing(true) - .setShowWhen(false) - .setOnlyAlertOnce(true) - .setContentIntent(contentPendingIntent) - .addAction( - R.drawable.ic_delete_24dp, - service.getString(R.string.notification_action_stop_v2ray), - stopV2RayPendingIntent - ) - .addAction( - R.drawable.ic_delete_24dp, - service.getString(R.string.title_service_restart), - restartV2RayPendingIntent - ) - - //mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) - - service.startForeground(NOTIFICATION_ID, mBuilder?.build()) - } - - /** - * Cancels the notification. - */ - fun cancelNotification() { - val service = getService() ?: return - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - service.stopForeground(Service.STOP_FOREGROUND_REMOVE) - } else { - service.stopForeground(true) - } - - mBuilder = null - speedNotificationJob?.cancel() - speedNotificationJob = null - mNotificationManager = null - } - - /** - * Stops the speed notification. - * @param currentConfig The current profile configuration. - */ - fun stopSpeedNotification(currentConfig: ProfileItem?) { - speedNotificationJob?.let { - it.cancel() - speedNotificationJob = null - updateNotification(currentConfig?.remarks, 0, 0) - } - } - - /** - * Creates a notification channel for Android O and above. - * @return The channel ID. - */ - @RequiresApi(Build.VERSION_CODES.O) - private fun createNotificationChannel(): String { - val channelId = AppConfig.RAY_NG_CHANNEL_ID - val channelName = AppConfig.RAY_NG_CHANNEL_NAME - val chan = NotificationChannel( - channelId, - channelName, NotificationManager.IMPORTANCE_HIGH - ) - chan.lightColor = Color.DKGRAY - chan.importance = NotificationManager.IMPORTANCE_NONE - chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE - getNotificationManager()?.createNotificationChannel(chan) - return channelId - } - - /** - * Updates the notification with the given content text and traffic data. - * @param contentText The content text. - * @param proxyTraffic The proxy traffic. - * @param directTraffic The direct traffic. - */ - private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) { - if (mBuilder != null) { - if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) { - mBuilder?.setSmallIcon(R.drawable.ic_stat_name) - } else if (proxyTraffic > directTraffic) { - mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy) - } else { - mBuilder?.setSmallIcon(R.drawable.ic_stat_direct) - } - mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) - mBuilder?.setContentText(contentText) - getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build()) - } - } - - /** - * Gets the notification manager. - * @return The notification manager. - */ - private fun getNotificationManager(): NotificationManager? { - if (mNotificationManager == null) { - val service = getService() ?: return null - mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - } - return mNotificationManager - } - - /** - * Appends the speed string to the given text. - * @param text The text to append to. - * @param name The name of the tag. - * @param up The uplink speed. - * @param down The downlink speed. - */ - private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) { - var n = name ?: "no tag" - n = n.substring(0, min(n.length, 6)) - text.append(n) - for (i in n.length..6 step 2) { - text.append("\t") - } - text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n") - } - - /** - * Gets the service instance. - * @return The service instance. - */ - private fun getService(): Service? { - return V2RayServiceManager.serviceControl?.get()?.getService() - } -} \ No newline at end of file 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 deleted file mode 100644 index e81e3615..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/ProcessService.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.v2ray.ang.service - -import android.content.Context -import android.util.Log -import com.v2ray.ang.AppConfig -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -class ProcessService { - private var process: Process? = null - - /** - * Runs a process with the given command. - * @param context The context. - * @param cmd The command to run. - */ - fun runProcess(context: Context, cmd: MutableList) { - Log.i(AppConfig.TAG, cmd.toString()) - - try { - val proBuilder = ProcessBuilder(cmd) - proBuilder.redirectErrorStream(true) - process = proBuilder - .directory(context.filesDir) - .start() - - CoroutineScope(Dispatchers.IO).launch { - Thread.sleep(50L) - Log.i(AppConfig.TAG, "runProcess check") - process?.waitFor() - Log.i(AppConfig.TAG, "runProcess exited") - } - Log.i(AppConfig.TAG, process.toString()) - - } catch (e: Exception) { - Log.e(AppConfig.TAG, e.toString(), e) - } - } - - /** - * Stops the running process. - */ - fun stopProcess() { - try { - Log.i(AppConfig.TAG, "runProcess destroy") - process?.destroy() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to destroy process", e) - } - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/ServiceControl.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/ServiceControl.kt deleted file mode 100644 index 085c72eb..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/ServiceControl.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.v2ray.ang.service - -import android.app.Service - -interface ServiceControl { - /** - * Gets the service instance. - * @return The service instance. - */ - fun getService(): Service - - /** - * Starts the service. - */ - fun startService() - - /** - * Stops the service. - */ - fun stopService() - - /** - * Protects the VPN socket. - * @param socket The socket to protect. - * @return True if the socket is protected, false otherwise. - */ - fun vpnProtect(socket: Int): Boolean -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdater.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdater.kt deleted file mode 100644 index 5f3f8172..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdater.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.v2ray.ang.service - -import android.annotation.SuppressLint -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.os.Build -import android.util.Log -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters -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.handler.AngConfigManager.updateConfigViaSub -import com.v2ray.ang.handler.MmkvManager - -object SubscriptionUpdater { - - class UpdateTask(context: Context, params: WorkerParameters) : - CoroutineWorker(context, params) { - - private val notificationManager = NotificationManagerCompat.from(applicationContext) - private val notification = - NotificationCompat.Builder(applicationContext, SUBSCRIPTION_UPDATE_CHANNEL) - .setWhen(0) - .setTicker("Update") - .setContentTitle(context.getString(R.string.title_pref_auto_update_subscription)) - .setSmallIcon(R.drawable.ic_stat_name) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - - /** - * Performs the subscription update work. - * @return The result of the work. - */ - @SuppressLint("MissingPermission") - override suspend fun doWork(): Result { - Log.i(AppConfig.TAG, "subscription automatic update starting") - - val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate } - - for (sub in subs) { - val subItem = sub.second - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - notification.setChannelId(SUBSCRIPTION_UPDATE_CHANNEL) - val channel = - NotificationChannel( - SUBSCRIPTION_UPDATE_CHANNEL, - SUBSCRIPTION_UPDATE_CHANNEL_NAME, - NotificationManager.IMPORTANCE_MIN - ) - notificationManager.createNotificationChannel(channel) - } - notificationManager.notify(3, notification.build()) - Log.i(AppConfig.TAG, "subscription automatic update: ---${subItem.remarks}") - updateConfigViaSub(Pair(sub.first, subItem)) - notification.setContentText("Updating ${subItem.remarks}") - } - notificationManager.cancel(3) - return Result.success() - } - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt deleted file mode 100644 index 4f42ca23..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt +++ /dev/null @@ -1,377 +0,0 @@ -package com.v2ray.ang.service - -import android.app.Service -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.os.Build -import android.util.Log -import androidx.core.content.ContextCompat -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ProfileItem -import com.v2ray.ang.extension.toast -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.handler.SettingsManager -import com.v2ray.ang.handler.SpeedtestManager -import com.v2ray.ang.handler.V2rayConfigManager -import com.v2ray.ang.util.MessageUtil -import com.v2ray.ang.util.PluginUtil -import com.v2ray.ang.util.Utils -import go.Seq -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import libv2ray.CoreCallbackHandler -import libv2ray.CoreController -import libv2ray.Libv2ray -import java.lang.ref.SoftReference - -object V2RayServiceManager { - - private val coreController: CoreController = Libv2ray.newCoreController(CoreCallback()) - private val mMsgReceive = ReceiveMessageHandler() - private var currentConfig: ProfileItem? = null - - var serviceControl: SoftReference? = null - set(value) { - field = value - Seq.setContext(value?.get()?.getService()?.applicationContext) - Libv2ray.initCoreEnv(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey()) - } - - /** - * Starts the V2Ray service from a toggle action. - * @param context The context from which the service is started. - * @return True if the service was started successfully, false otherwise. - */ - fun startVServiceFromToggle(context: Context): Boolean { - if (MmkvManager.getSelectServer().isNullOrEmpty()) { - context.toast(R.string.app_tile_first_use) - return false - } - startContextService(context) - return true - } - - /** - * Starts the V2Ray service. - * @param context The context from which the service is started. - * @param guid The GUID of the server configuration to use (optional). - */ - fun startVService(context: Context, guid: String? = null) { - if (guid != null) { - MmkvManager.setSelectServer(guid) - } - startContextService(context) - } - - /** - * Stops the V2Ray service. - * @param context The context from which the service is stopped. - */ - fun stopVService(context: Context) { - context.toast(R.string.toast_services_stop) - MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "") - } - - /** - * Checks if the V2Ray service is running. - * @return True if the service is running, false otherwise. - */ - fun isRunning() = coreController.isRunning - - /** - * Gets the name of the currently running server. - * @return The name of the running server. - */ - fun getRunningServerName() = currentConfig?.remarks.orEmpty() - - /** - * Starts the context service for V2Ray. - * Chooses between VPN service or Proxy-only service based on user settings. - * @param context The context from which the service is started. - */ - private fun startContextService(context: Context) { - if (coreController.isRunning) { - return - } - val guid = MmkvManager.getSelectServer() ?: return - val config = MmkvManager.decodeServerConfig(guid) ?: return - if (config.configType != EConfigType.CUSTOM - && !Utils.isValidUrl(config.server) - && !Utils.isPureIpAddress(config.server.orEmpty()) - ) 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 ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: AppConfig.VPN) == AppConfig.VPN) { - Intent(context.applicationContext, V2RayVpnService::class.java) - } else { - Intent(context.applicationContext, V2RayProxyOnlyService::class.java) - } - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { - context.startForegroundService(intent) - } else { - context.startService(intent) - } - } - - /** - * 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)`. - * Starts the V2Ray core service. - */ - fun startCoreLoop(): Boolean { - if (coreController.isRunning) { - return false - } - - val service = getService() ?: return false - val guid = MmkvManager.getSelectServer() ?: return false - val config = MmkvManager.decodeServerConfig(guid) ?: return false - val result = V2rayConfigManager.getV2rayConfig(service, guid) - if (!result.status) - return false - - try { - val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE) - mFilter.addAction(Intent.ACTION_SCREEN_ON) - mFilter.addAction(Intent.ACTION_SCREEN_OFF) - mFilter.addAction(Intent.ACTION_USER_PRESENT) - ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags()) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e) - return false - } - - currentConfig = config - - try { - coreController.startLoop(result.content) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to start Core loop", e) - return false - } - - if (coreController.isRunning == false) { - MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "") - NotificationService.cancelNotification() - return false - } - - try { - MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "") - NotificationService.showNotification(currentConfig) - NotificationService.startSpeedNotification(currentConfig) - - PluginUtil.runPlugin(service, config, result.socksPort) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to startup service", e) - return false - } - return true - } - - /** - * Stops the V2Ray core service. - * Unregisters broadcast receivers, stops notifications, and shuts down plugins. - * @return True if the core was stopped successfully, false otherwise. - */ - fun stopCoreLoop(): Boolean { - val service = getService() ?: return false - - if (coreController.isRunning) { - CoroutineScope(Dispatchers.IO).launch { - try { - coreController.stopLoop() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e) - } - } - } - - MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "") - NotificationService.cancelNotification() - - try { - service.unregisterReceiver(mMsgReceive) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e) - } - PluginUtil.stopPlugin() - - return true - } - - /** - * Queries the statistics for a given tag and link. - * @param tag The tag to query. - * @param link The link to query. - * @return The statistics value. - */ - fun queryStats(tag: String, link: String): Long { - return coreController.queryStats(tag, link) - } - - /** - * Measures the connection delay for the current V2Ray configuration. - * Tests with primary URL first, then falls back to alternative URL if needed. - * Also fetches remote IP information if the delay test was successful. - */ - private fun measureV2rayDelay() { - if (coreController.isRunning == false) { - return - } - - CoroutineScope(Dispatchers.IO).launch { - val service = getService() ?: return@launch - var time = -1L - var errorStr = "" - - try { - time = coreController.measureDelay(SettingsManager.getDelayTestUrl()) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e) - errorStr = e.message?.substringAfter("\":") ?: "empty message" - } - if (time == -1L) { - try { - time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true)) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e) - errorStr = e.message?.substringAfter("\":") ?: "empty message" - } - } - - val result = if (time >= 0) { - service.getString(R.string.connection_test_available, time) - } else { - service.getString(R.string.connection_test_error, errorStr) - } - MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result) - - // Only fetch IP info if the delay test was successful - if (time >= 0) { - SpeedtestManager.getRemoteIPInfo()?.let { ip -> - MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip") - } - } - } - } - - /** - * Gets the current service instance. - * @return The current service instance, or null if not available. - */ - private fun getService(): Service? { - return serviceControl?.get()?.getService() - } - - /** - * Core callback handler implementation for handling V2Ray core events. - * Handles startup, shutdown, socket protection, and status emission. - */ - private class CoreCallback : CoreCallbackHandler { - /** - * Called when V2Ray core starts up. - * @return 0 for success, any other value for failure. - */ - override fun startup(): Long { - return 0 - } - - /** - * Called when V2Ray core shuts down. - * @return 0 for success, any other value for failure. - */ - override fun shutdown(): Long { - val serviceControl = serviceControl?.get() ?: return -1 - return try { - serviceControl.stopService() - 0 - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to stop service in callback", e) - -1 - } - } - - /** - * Called when V2Ray core emits status information. - * @param l Status code. - * @param s Status message. - * @return Always returns 0. - */ - override fun onEmitStatus(l: Long, s: String?): Long { - return 0 - } - } - - /** - * Broadcast receiver for handling messages sent to the service. - * Handles registration, service control, and screen events. - */ - private class ReceiveMessageHandler : BroadcastReceiver() { - /** - * Handles received broadcast messages. - * Processes service control messages and screen state changes. - * @param ctx The context in which the receiver is running. - * @param intent The intent being received. - */ - override fun onReceive(ctx: Context?, intent: Intent?) { - val serviceControl = serviceControl?.get() ?: return - when (intent?.getIntExtra("key", 0)) { - AppConfig.MSG_REGISTER_CLIENT -> { - if (coreController.isRunning) { - MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "") - } else { - MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "") - } - } - - AppConfig.MSG_UNREGISTER_CLIENT -> { - // nothing to do - } - - AppConfig.MSG_STATE_START -> { - // nothing to do - } - - AppConfig.MSG_STATE_STOP -> { - Log.i(AppConfig.TAG, "Stop Service") - serviceControl.stopService() - } - - AppConfig.MSG_STATE_RESTART -> { - Log.i(AppConfig.TAG, "Restart Service") - serviceControl.stopService() - Thread.sleep(500L) - startVService(serviceControl.getService()) - } - - AppConfig.MSG_MEASURE_DELAY -> { - measureV2rayDelay() - } - } - - when (intent?.action) { - Intent.ACTION_SCREEN_OFF -> { - Log.i(AppConfig.TAG, "SCREEN_OFF, stop querying stats") - NotificationService.stopSpeedNotification(currentConfig) - } - - Intent.ACTION_SCREEN_ON -> { - Log.i(AppConfig.TAG, "SCREEN_ON, start querying stats") - NotificationService.startSpeedNotification(currentConfig) - } - } - } - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt deleted file mode 100644 index 3fef1ae1..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.v2ray.ang.service - -import android.app.Service -import android.content.Intent -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.SpeedtestManager -import com.v2ray.ang.handler.V2rayConfigManager -import com.v2ray.ang.util.MessageUtil -import com.v2ray.ang.util.PluginUtil -import com.v2ray.ang.util.Utils -import go.Seq -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.launch -import libv2ray.Libv2ray -import java.util.concurrent.Executors - -class V2RayTestService : Service() { - private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) } - - /** - * Initializes the V2Ray environment. - */ - override fun onCreate() { - super.onCreate() - Seq.setContext(this) - Libv2ray.initCoreEnv(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey()) - } - - /** - * Handles the start command for the service. - * @param intent The intent. - * @param flags The flags. - * @param startId The start ID. - * @return The start mode. - */ - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - when (intent?.getIntExtra("key", 0)) { - MSG_MEASURE_CONFIG -> { - val guid = intent.serializable("content") ?: "" - realTestScope.launch { - val result = startRealPing(guid) - MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result)) - } - } - - MSG_MEASURE_CONFIG_CANCEL -> { - realTestScope.coroutineContext[Job]?.cancelChildren() - } - } - return super.onStartCommand(intent, flags, startId) - } - - /** - * Binds the service. - * @param intent The intent. - * @return The binder. - */ - override fun onBind(intent: Intent?): IBinder? { - return null - } - - /** - * Starts the real ping test. - * @param guid The GUID of the configuration. - * @return The ping result. - */ - private fun startRealPing(guid: String): Long { - val retFailure = -1L - - val config = MmkvManager.decodeServerConfig(guid) ?: return retFailure - if (config.configType == EConfigType.HYSTERIA2) { - val delay = PluginUtil.realPingHy2(this, config) - return delay - } else { - val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(this, guid) - if (!configResult.status) { - return retFailure - } - return SpeedtestManager.realPing(configResult.content) - } - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt deleted file mode 100644 index 1931cb45..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt +++ /dev/null @@ -1,201 +0,0 @@ -package com.v2ray.ang.ui - -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.ContextCompat -import androidx.core.content.FileProvider -import com.tencent.mmkv.MMKV -import com.v2ray.ang.AppConfig -import com.v2ray.ang.BuildConfig -import com.v2ray.ang.R -import com.v2ray.ang.databinding.ActivityAboutBinding -import com.v2ray.ang.extension.toast -import com.v2ray.ang.extension.toastError -import com.v2ray.ang.extension.toastSuccess -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.handler.SpeedtestManager -import com.v2ray.ang.util.Utils -import com.v2ray.ang.util.ZipUtil -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)) } - - private val requestPermissionLauncher = - registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> - if (isGranted) { - try { - showFileChooser() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to show file chooser", e) - } - } else { - toast(R.string.toast_permission_denied) - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - - title = getString(R.string.title_about) - - binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir) - - binding.layoutBackup.setOnClickListener { - val ret = backupConfiguration(extDir.absolutePath) - if (ret.first) { - toastSuccess(R.string.toast_success) - } else { - toastError(R.string.toast_failure) - } - } - - binding.layoutShare.setOnClickListener { - val ret = backupConfiguration(cacheDir.absolutePath) - if (ret.first) { - startActivity( - Intent.createChooser( - Intent(Intent.ACTION_SEND).setType("application/zip") - .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - .putExtra( - Intent.EXTRA_STREAM, - FileProvider.getUriForFile( - this, BuildConfig.APPLICATION_ID + ".cache", File(ret.second) - ) - ), getString(R.string.title_configuration_share) - ) - ) - } else { - toastError(R.string.toast_failure) - } - } - - binding.layoutRestore.setOnClickListener { - val permission = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Manifest.permission.READ_MEDIA_IMAGES - } else { - Manifest.permission.READ_EXTERNAL_STORAGE - } - - if (ContextCompat.checkSelfPermission(this, permission) == android.content.pm.PackageManager.PERMISSION_GRANTED) { - try { - showFileChooser() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to show file chooser", e) - } - } else { - requestPermissionLauncher.launch(permission) - } - } - - binding.layoutSoureCcode.setOnClickListener { - Utils.openUri(this, AppConfig.APP_URL) - } - - binding.layoutFeedback.setOnClickListener { - Utils.openUri(this, AppConfig.APP_ISSUES_URL) - } - - 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") { dialog, _ -> dialog.dismiss() } - .show() - } - - binding.layoutTgChannel.setOnClickListener { - Utils.openUri(this, AppConfig.TG_CHANNEL_URL) - } - - binding.layoutPrivacyPolicy.setOnClickListener { - Utils.openUri(this, AppConfig.APP_PRIVACY_POLICY) - } - - "v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also { - binding.tvVersion.text = it - } - } - - private fun backupConfiguration(outputZipFilePos: String): Pair { - val dateFormated = SimpleDateFormat( - "yyyy-MM-dd-HH-mm-ss", - Locale.getDefault() - ).format(System.currentTimeMillis()) - val folderName = "${getString(R.string.app_name)}_${dateFormated}" - val backupDir = this.cacheDir.absolutePath + "/$folderName" - val outputZipFilePath = "$outputZipFilePos/$folderName.zip" - - val count = MMKV.backupAllToDirectory(backupDir) - if (count <= 0) { - return Pair(false, "") - } - - if (ZipUtil.zipFromFolder(backupDir, outputZipFilePath)) { - return Pair(true, outputZipFilePath) - } else { - return Pair(false, "") - } - } - - private fun restoreConfiguration(zipFile: File): Boolean { - val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}" - - if (!ZipUtil.unzipToFolder(zipFile, backupDir)) { - return false - } - - val count = MMKV.restoreAllFromDirectory(backupDir) - return count > 0 - } - - private fun showFileChooser() { - 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.TAG, "File chooser activity not found", ex) - toast(R.string.toast_require_file_manager) - } - } - - private val chooseFile = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - val uri = result.data?.data - if (result.resultCode == RESULT_OK && uri != null) { - 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)) { - toastSuccess(R.string.toast_success) - } else { - toastError(R.string.toast_failure) - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Error during file restore", e) - toastError(R.string.toast_failure) - } - } - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt deleted file mode 100644 index 0cd7f647..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.v2ray.ang.ui - -import android.content.Context -import android.os.Build -import android.os.Bundle -import android.view.MenuItem -import androidx.annotation.RequiresApi -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat -import androidx.core.view.WindowCompat -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.RecyclerView -import com.v2ray.ang.handler.SettingsManager -import com.v2ray.ang.helper.CustomDividerItemDecoration -import com.v2ray.ang.util.MyContextWrapper -import com.v2ray.ang.util.Utils - - -abstract class BaseActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - if (!Utils.getDarkModeStatus(this)) { - WindowCompat.getInsetsController(window, window.decorView).apply { - isAppearanceLightStatusBars = true - } - } - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - android.R.id.home -> { - // Handles the home button press by delegating to the onBackPressedDispatcher. - // This ensures consistent back navigation behavior. - onBackPressedDispatcher.onBackPressed() - true - } - - else -> super.onOptionsItemSelected(item) - } - - @RequiresApi(Build.VERSION_CODES.N) - override fun attachBaseContext(newBase: Context?) { - super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, SettingsManager.getLocale())) - } - - /** - * Adds a custom divider to a RecyclerView. - * - * @param recyclerView The target RecyclerView to which the divider will be added. - * @param context The context used to access resources. - * @param drawableResId The resource ID of the drawable to be used as the divider. - * @param orientation The orientation of the divider (DividerItemDecoration.VERTICAL or DividerItemDecoration.HORIZONTAL). - */ - fun addCustomDividerToRecyclerView(recyclerView: RecyclerView, context: Context?, drawableResId: Int, orientation: Int = DividerItemDecoration.VERTICAL) { - // Get the drawable from resources - val drawable = ContextCompat.getDrawable(context!!, drawableResId) - requireNotNull(drawable) { "Drawable resource not found" } - - // Create a DividerItemDecoration with the specified orientation - val dividerItemDecoration = CustomDividerItemDecoration(drawable, orientation) - - // Add the divider to the RecyclerView - recyclerView.addItemDecoration(dividerItemDecoration) - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt deleted file mode 100644 index a9b698c5..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.v2ray.ang.ui - -import android.os.Bundle -import android.util.Log -import androidx.appcompat.app.AlertDialog -import androidx.lifecycle.lifecycleScope -import com.v2ray.ang.AppConfig -import com.v2ray.ang.BuildConfig -import com.v2ray.ang.R -import com.v2ray.ang.databinding.ActivityCheckUpdateBinding -import com.v2ray.ang.dto.CheckUpdateResult -import com.v2ray.ang.extension.toast -import com.v2ray.ang.extension.toastError -import com.v2ray.ang.extension.toastSuccess -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.handler.SpeedtestManager -import com.v2ray.ang.handler.UpdateCheckerManager -import com.v2ray.ang.util.Utils -import kotlinx.coroutines.launch - -class CheckUpdateActivity : BaseActivity() { - - private val binding by lazy { ActivityCheckUpdateBinding.inflate(layoutInflater) } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - - title = getString(R.string.update_check_for_update) - - binding.layoutCheckUpdate.setOnClickListener { - checkForUpdates(binding.checkPreRelease.isChecked) - } - - binding.checkPreRelease.setOnCheckedChangeListener { _, isChecked -> - MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, isChecked) - } - binding.checkPreRelease.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, false) - - "v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also { - binding.tvVersion.text = it - } - - checkForUpdates(binding.checkPreRelease.isChecked) - } - - private fun checkForUpdates(includePreRelease: Boolean) { - toast(R.string.update_checking_for_update) - - lifecycleScope.launch { - try { - val result = UpdateCheckerManager.checkForUpdate(includePreRelease) - if (result.hasUpdate) { - showUpdateDialog(result) - } else { - toastSuccess(R.string.update_already_latest_version) - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}") - toastError(e.message ?: getString(R.string.toast_failure)) - } - } - } - - private fun showUpdateDialog(result: CheckUpdateResult) { - AlertDialog.Builder(this) - .setTitle(getString(R.string.update_new_version_found, result.latestVersion)) - .setMessage(result.releaseNotes) - .setPositiveButton(R.string.update_now) { _, _ -> - result.downloadUrl?.let { - Utils.openUri(this, it) - } - } - .setNegativeButton(android.R.string.cancel, null) - .show() - } -} \ No newline at end of file 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 deleted file mode 100644 index 436f5913..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt +++ /dev/null @@ -1,156 +0,0 @@ -package com.v2ray.ang.ui - -import android.annotation.SuppressLint -import android.os.Bundle -import android.util.Log -import android.view.Menu -import android.view.MenuItem -import androidx.appcompat.widget.SearchView -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import com.v2ray.ang.AppConfig -import com.v2ray.ang.AppConfig.ANG_PACKAGE -import com.v2ray.ang.R -import com.v2ray.ang.databinding.ActivityLogcatBinding -import com.v2ray.ang.extension.toastSuccess -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) } - - private 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) - addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) - binding.recyclerView.adapter = adapter - - 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() - refreshData() - binding.refreshLayout.isRefreshing = false - } - } - } catch (e: IOException) { - Log.e(AppConfig.TAG, "Failed to get logcat", e) - } - } - - private fun clearLogcat() { - try { - lifecycleScope.launch(Dispatchers.Default) { - val lst = LinkedHashSet() - lst.add("logcat") - lst.add("-c") - withContext(Dispatchers.IO) { - val process = Runtime.getRuntime().exec(lst.toTypedArray()) - process.waitFor() - } - launch(Dispatchers.Main) { - logsetsAll.clear() - logsets.clear() - refreshData() - } - } - } catch (e: IOException) { - Log.e(AppConfig.TAG, "Failed to clear logcat", e) - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - 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")) - toastSuccess(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() - } - - refreshData() - return true - } - - override fun onRefresh() { - getLogcat() - } - - @SuppressLint("NotifyDataSetChanged") - fun refreshData() { - adapter.notifyDataSetChanged() - } -} \ 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 deleted file mode 100644 index e83714d5..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatRecyclerAdapter.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.v2ray.ang.ui - -import android.util.Log -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.v2ray.ang.AppConfig -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) { - try { - val log = mActivity.logsets[position] - if (log.isEmpty()) { - holder.itemSubSettingBinding.logTag.text = "" - holder.itemSubSettingBinding.logContent.text = "" - } else { - val content = log.split("):", limit = 2) - holder.itemSubSettingBinding.logTag.text = content.first().split("(", limit = 2).first().trim() - holder.itemSubSettingBinding.logContent.text = if (content.count() > 1) content.last().trim() else "" - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Error binding log view data", e) - } - } - - 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/java/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt deleted file mode 100644 index 0c7584d8..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt +++ /dev/null @@ -1,695 +0,0 @@ -package com.v2ray.ang.ui - -import android.Manifest -import android.annotation.SuppressLint -import android.content.Intent -import android.content.pm.PackageManager -import android.content.res.ColorStateList -import android.net.Uri -import android.net.VpnService -import android.os.Build -import android.os.Bundle -import android.util.Log -import android.view.KeyEvent -import android.view.Menu -import android.view.MenuItem -import androidx.activity.OnBackPressedCallback -import androidx.activity.result.contract.ActivityResultContracts -import androidx.activity.viewModels -import androidx.appcompat.app.ActionBarDrawerToggle -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.SearchView -import androidx.core.content.ContextCompat -import androidx.core.view.GravityCompat -import androidx.core.view.isVisible -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.ItemTouchHelper -import com.google.android.material.navigation.NavigationView -import com.google.android.material.tabs.TabLayout -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.extension.toastError -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.Utils -import com.v2ray.ang.viewmodel.MainViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener { - private val binding by lazy { - ActivityMainBinding.inflate(layoutInflater) - } - - private val adapter by lazy { MainRecyclerAdapter(this) } - private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - startV2Ray() - } - } - private val requestSubSettingActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - initGroupTab() - } - private val tabGroupListener = object : TabLayout.OnTabSelectedListener { - override fun onTabSelected(tab: TabLayout.Tab?) { - val selectId = tab?.tag.toString() - if (selectId != mainViewModel.subscriptionId) { - mainViewModel.subscriptionIdChanged(selectId) - } - } - - override fun onTabUnselected(tab: TabLayout.Tab?) { - } - - override fun onTabReselected(tab: TabLayout.Tab?) { - } - } - private var mItemTouchHelper: ItemTouchHelper? = null - val mainViewModel: MainViewModel by viewModels() - - // register activity result for requesting permission - private val requestPermissionLauncher = - registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - when (pendingAction) { - Action.IMPORT_QR_CODE_CONFIG -> - scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java)) - - Action.READ_CONTENT_FROM_URI -> - chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply { - type = "*/*" - addCategory(Intent.CATEGORY_OPENABLE) - }, getString(R.string.title_file_chooser))) - - Action.POST_NOTIFICATIONS -> {} - else -> {} - } - } else { - toast(R.string.toast_permission_denied) - } - pendingAction = Action.NONE - } - - private var pendingAction: Action = Action.NONE - - enum class Action { - NONE, - IMPORT_QR_CODE_CONFIG, - READ_CONTENT_FROM_URI, - POST_NOTIFICATIONS - } - - private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - val uri = it.data?.data - if (it.resultCode == RESULT_OK && uri != null) { - readContentFromUri(uri) - } - } - - private val scanQRCodeForConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - importBatchConfig(it.data?.getStringExtra("SCAN_RESULT")) - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - title = getString(R.string.title_server) - setSupportActionBar(binding.toolbar) - - binding.fab.setOnClickListener { - if (mainViewModel.isRunning.value == true) { - V2RayServiceManager.stopVService(this) - } else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) { - val intent = VpnService.prepare(this) - if (intent == null) { - startV2Ray() - } else { - requestVpnPermission.launch(intent) - } - } else { - startV2Ray() - } - } - binding.layoutTest.setOnClickListener { - if (mainViewModel.isRunning.value == true) { - setTestState(getString(R.string.connection_test_testing)) - mainViewModel.testCurrentServerRealPing() - } else { -// tv_test_state.text = getString(R.string.connection_test_fail) - } - } - - binding.recyclerView.setHasFixedSize(true) - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)) { - binding.recyclerView.layoutManager = GridLayoutManager(this, 2) - } else { - binding.recyclerView.layoutManager = GridLayoutManager(this, 1) - } - addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) - binding.recyclerView.adapter = adapter - - 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 - ) - binding.drawerLayout.addDrawerListener(toggle) - toggle.syncState() - binding.navView.setNavigationItemSelectedListener(this) - - initGroupTab() - setupViewModel() - migrateLegacy() - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - pendingAction = Action.POST_NOTIFICATIONS - requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) - } - } - - onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { - binding.drawerLayout.closeDrawer(GravityCompat.START) - } else { - isEnabled = false - onBackPressedDispatcher.onBackPressed() - isEnabled = true - } - } - }) - } - - @SuppressLint("NotifyDataSetChanged") - private fun setupViewModel() { - mainViewModel.updateListAction.observe(this) { index -> - if (index >= 0) { - adapter.notifyItemChanged(index) - } else { - adapter.notifyDataSetChanged() - } - } - mainViewModel.updateTestResultAction.observe(this) { setTestState(it) } - mainViewModel.isRunning.observe(this) { isRunning -> - adapter.isRunning = isRunning - if (isRunning) { - binding.fab.setImageResource(R.drawable.ic_stop_24dp) - binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_active)) - setTestState(getString(R.string.connection_connected)) - binding.layoutTest.isFocusable = true - } else { - binding.fab.setImageResource(R.drawable.ic_play_24dp) - binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive)) - setTestState(getString(R.string.connection_not_connected)) - binding.layoutTest.isFocusable = false - } - } - mainViewModel.startListenBroadcast() - 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() { - binding.tabGroup.removeOnTabSelectedListener(tabGroupListener) - binding.tabGroup.removeAllTabs() - binding.tabGroup.isVisible = false - - val (listId, listRemarks) = mainViewModel.getSubscriptions(this) - if (listId == null || listRemarks == null) { - return - } - - for (it in listRemarks.indices) { - val tab = binding.tabGroup.newTab() - tab.text = listRemarks[it] - tab.tag = listId[it] - binding.tabGroup.addTab(tab) - } - val selectIndex = - listId.indexOf(mainViewModel.subscriptionId).takeIf { it >= 0 } ?: (listId.count() - 1) - binding.tabGroup.selectTab(binding.tabGroup.getTabAt(selectIndex)) - binding.tabGroup.addOnTabSelectedListener(tabGroupListener) - binding.tabGroup.isVisible = true - } - - private fun startV2Ray() { - if (MmkvManager.getSelectServer().isNullOrEmpty()) { - toast(R.string.title_file_chooser) - return - } - V2RayServiceManager.startVService(this) - } - - private fun restartV2Ray() { - if (mainViewModel.isRunning.value == true) { - V2RayServiceManager.stopVService(this) - } - lifecycleScope.launch { - delay(500) - startV2Ray() - } - } - - public override fun onResume() { - super.onResume() - mainViewModel.reloadServerList() - } - - public override fun onPause() { - super.onPause() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_main, 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 { - mainViewModel.filterConfig(newText.orEmpty()) - return false - } - }) - - searchView.setOnCloseListener { - mainViewModel.filterConfig("") - false - } - } - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.import_qrcode -> { - importQRcode() - true - } - - R.id.import_clipboard -> { - importClipboard() - true - } - - R.id.import_local -> { - importConfigLocal() - true - } - - R.id.import_manually_vmess -> { - importManually(EConfigType.VMESS.value) - true - } - - R.id.import_manually_vless -> { - importManually(EConfigType.VLESS.value) - true - } - - R.id.import_manually_ss -> { - importManually(EConfigType.SHADOWSOCKS.value) - true - } - - R.id.import_manually_socks -> { - importManually(EConfigType.SOCKS.value) - true - } - - R.id.import_manually_http -> { - importManually(EConfigType.HTTP.value) - true - } - - R.id.import_manually_trojan -> { - importManually(EConfigType.TROJAN.value) - true - } - - R.id.import_manually_wireguard -> { - importManually(EConfigType.WIREGUARD.value) - true - } - - R.id.import_manually_hysteria2 -> { - importManually(EConfigType.HYSTERIA2.value) - true - } - - R.id.export_all -> { - exportAll() - true - } - - 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 - } - - R.id.service_restart -> { - restartV2Ray() - true - } - - R.id.del_all_config -> { - delAllConfig() - true - } - - R.id.del_duplicate_config -> { - delDuplicateConfig() - true - } - - R.id.del_invalid_config -> { - delInvalidConfig() - true - } - - R.id.sort_by_test_results -> { - sortByTestResults() - true - } - - R.id.sub_update -> { - importConfigViaSub() - true - } - - - else -> super.onOptionsItemSelected(item) - } - - private fun importManually(createConfigType: Int) { - startActivity( - Intent() - .putExtra("createConfigType", createConfigType) - .putExtra("subscriptionId", mainViewModel.subscriptionId) - .setClass(this, ServerActivity::class.java) - ) - } - - /** - * import config from qrcode - */ - private fun importQRcode(): Boolean { - val permission = Manifest.permission.CAMERA - if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { - scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java)) - } else { - pendingAction = Action.IMPORT_QR_CODE_CONFIG - requestPermissionLauncher.launch(permission) - } - return true - } - - /** - * import config from clipboard - */ - private fun importClipboard() - : Boolean { - try { - val clipboard = Utils.getClipboard(this) - importBatchConfig(clipboard) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to import config from clipboard", e) - return false - } - return true - } - - private fun importBatchConfig(server: String?) { - binding.pbWaiting.show() - - lifecycleScope.launch(Dispatchers.IO) { - 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 -> toastError(R.string.toast_failure) - } - binding.pbWaiting.hide() - } - } catch (e: Exception) { - withContext(Dispatchers.Main) { - toastError(R.string.toast_failure) - binding.pbWaiting.hide() - } - Log.e(AppConfig.TAG, "Failed to import batch config", e) - } - } - } - - /** - * import config from local config file - */ - private fun importConfigLocal(): Boolean { - try { - showFileChooser() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to import config from local file", e) - return false - } - return true - } - - - /** - * import config from sub - */ - private fun importConfigViaSub(): Boolean { - binding.pbWaiting.show() - - lifecycleScope.launch(Dispatchers.IO) { - val count = mainViewModel.updateConfigViaSubAll() - delay(500L) - launch(Dispatchers.Main) { - if (count > 0) { - toast(getString(R.string.title_update_config_count, count)) - mainViewModel.reloadServerList() - } else { - toastError(R.string.toast_failure) - } - binding.pbWaiting.hide() - } - } - return true - } - - private fun exportAll() { - binding.pbWaiting.show() - lifecycleScope.launch(Dispatchers.IO) { - val ret = mainViewModel.exportAllServer() - launch(Dispatchers.Main) { - if (ret > 0) - toast(getString(R.string.title_export_config_count, ret)) - else - toastError(R.string.toast_failure) - binding.pbWaiting.hide() - } - } - } - - private fun delAllConfig() { - AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) - .setPositiveButton(android.R.string.ok) { _, _ -> - binding.pbWaiting.show() - lifecycleScope.launch(Dispatchers.IO) { - 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.cancel) { _, _ -> - //do noting - } - .show() - } - - private fun delDuplicateConfig() { - AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) - .setPositiveButton(android.R.string.ok) { _, _ -> - binding.pbWaiting.show() - lifecycleScope.launch(Dispatchers.IO) { - val ret = mainViewModel.removeDuplicateServer() - launch(Dispatchers.Main) { - mainViewModel.reloadServerList() - toast(getString(R.string.title_del_duplicate_config_count, ret)) - binding.pbWaiting.hide() - } - } - } - .setNegativeButton(android.R.string.cancel) { _, _ -> - //do noting - } - .show() - } - - private fun delInvalidConfig() { - AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm) - .setPositiveButton(android.R.string.ok) { _, _ -> - binding.pbWaiting.show() - lifecycleScope.launch(Dispatchers.IO) { - 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.cancel) { _, _ -> - //do noting - } - .show() - } - - private fun sortByTestResults() { - binding.pbWaiting.show() - lifecycleScope.launch(Dispatchers.IO) { - mainViewModel.sortByTestResults() - launch(Dispatchers.Main) { - mainViewModel.reloadServerList() - binding.pbWaiting.hide() - } - } - } - - /** - * show file chooser - */ - private fun showFileChooser() { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.type = "*/*" - intent.addCategory(Intent.CATEGORY_OPENABLE) - - val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Manifest.permission.READ_MEDIA_IMAGES - } else { - Manifest.permission.READ_EXTERNAL_STORAGE - } - - if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { - pendingAction = Action.READ_CONTENT_FROM_URI - chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) - } else { - requestPermissionLauncher.launch(permission) - } - } - - /** - * read content from uri - */ - private fun readContentFromUri(uri: Uri) { - val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Manifest.permission.READ_MEDIA_IMAGES - } else { - Manifest.permission.READ_EXTERNAL_STORAGE - } - - if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { - try { - contentResolver.openInputStream(uri).use { input -> - importBatchConfig(input?.bufferedReader()?.readText()) - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to read content from URI", e) - } - } else { - requestPermissionLauncher.launch(permission) - } - } - - private fun setTestState(content: String?) { - binding.tvTestState.text = content - } - -// val mConnection = object : ServiceConnection { -// override fun onServiceDisconnected(name: ComponentName?) { -// } -// -// override fun onServiceConnected(name: ComponentName?, service: IBinder?) { -// sendMsg(AppConfig.MSG_REGISTER_CLIENT, "") -// } -// } - - override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { - if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) { - moveTaskToBack(false) - return true - } - return super.onKeyDown(keyCode, event) - } - - - override fun onNavigationItemSelected(item: MenuItem): Boolean { - // Handle navigation view item clicks here. - when (item.itemId) { - R.id.sub_setting -> requestSubSettingActivity.launch(Intent(this, SubSettingActivity::class.java)) - R.id.per_app_proxy_settings -> startActivity(Intent(this, PerAppProxyActivity::class.java)) - R.id.routing_setting -> requestSubSettingActivity.launch(Intent(this, RoutingSettingActivity::class.java)) - R.id.user_asset_setting -> startActivity(Intent(this, UserAssetActivity::class.java)) - R.id.settings -> startActivity( - Intent(this, SettingsActivity::class.java) - .putExtra("isRunning", mainViewModel.isRunning.value == true) - ) - - R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.APP_PROMOTION_URL)}?t=${System.currentTimeMillis()}") - R.id.logcat -> startActivity(Intent(this, LogcatActivity::class.java)) - R.id.check_for_update -> startActivity(Intent(this, CheckUpdateActivity::class.java)) - R.id.about -> startActivity(Intent(this, AboutActivity::class.java)) - } - - binding.drawerLayout.closeDrawer(GravityCompat.START) - return true - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt deleted file mode 100644 index e7ea6211..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt +++ /dev/null @@ -1,362 +0,0 @@ -package com.v2ray.ang.ui - -import android.content.Intent -import android.graphics.Color -import android.text.TextUtils -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.RecyclerView -import com.v2ray.ang.AngApplication.Companion.application -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -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.ProfileItem -import com.v2ray.ang.extension.toast -import com.v2ray.ang.extension.toastError -import com.v2ray.ang.extension.toastSuccess -import com.v2ray.ang.handler.AngConfigManager -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.helper.ItemTouchHelperAdapter -import com.v2ray.ang.helper.ItemTouchHelperViewHolder -import com.v2ray.ang.service.V2RayServiceManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter(), ItemTouchHelperAdapter { - companion object { - private const val VIEW_TYPE_ITEM = 1 - private const val VIEW_TYPE_FOOTER = 2 - } - - private var mActivity: MainActivity = activity - private val share_method: Array by lazy { - mActivity.resources.getStringArray(R.array.share_method) - } - private val share_method_more: Array by lazy { - mActivity.resources.getStringArray(R.array.share_method_more) - } - var isRunning = false - private val doubleColumnDisplay = MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false) - - /** - * Gets the total number of items in the adapter (servers count + footer view) - * @return The total item count - */ - override fun getItemCount() = mActivity.mainViewModel.serversCache.size + 1 - - override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { - if (holder is MainViewHolder) { - val guid = mActivity.mainViewModel.serversCache[position].guid - val profile = mActivity.mainViewModel.serversCache[position].profile - val isCustom = profile.configType == EConfigType.CUSTOM - - holder.itemView.setBackgroundColor(Color.TRANSPARENT) - - //Name address - holder.itemMainBinding.tvName.text = profile.remarks - holder.itemMainBinding.tvStatistics.text = getAddress(profile) - holder.itemMainBinding.tvType.text = profile.configType.name - - //TestResult - val aff = MmkvManager.decodeServerAffiliationInfo(guid) - holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty() - if ((aff?.testDelayMillis ?: 0L) < 0L) { - holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed)) - } else { - holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing)) - } - - //layoutIndicator - if (guid == MmkvManager.getSelectServer()) { - holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent) - } else { - holder.itemMainBinding.layoutIndicator.setBackgroundResource(0) - } - - //subscription remarks - val subRemarks = getSubscriptionRemarks(profile) - holder.itemMainBinding.tvSubscription.text = subRemarks - holder.itemMainBinding.layoutSubscription.visibility = if (subRemarks.isEmpty()) View.GONE else View.VISIBLE - - //layout - if (doubleColumnDisplay) { - holder.itemMainBinding.layoutShare.visibility = View.GONE - holder.itemMainBinding.layoutEdit.visibility = View.GONE - holder.itemMainBinding.layoutRemove.visibility = View.GONE - holder.itemMainBinding.layoutMore.visibility = View.VISIBLE - - //share method - val shareOptions = if (isCustom) share_method_more.asList().takeLast(3) else share_method_more.asList() - - holder.itemMainBinding.layoutMore.setOnClickListener { - shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0) - } - } else { - holder.itemMainBinding.layoutShare.visibility = View.VISIBLE - holder.itemMainBinding.layoutEdit.visibility = View.VISIBLE - holder.itemMainBinding.layoutRemove.visibility = View.VISIBLE - holder.itemMainBinding.layoutMore.visibility = View.GONE - - //share method - val shareOptions = if (isCustom) share_method.asList().takeLast(1) else share_method.asList() - - holder.itemMainBinding.layoutShare.setOnClickListener { - shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0) - } - - holder.itemMainBinding.layoutEdit.setOnClickListener { - editServer(guid, profile) - } - holder.itemMainBinding.layoutRemove.setOnClickListener { - removeServer(guid, position) - } - } - - holder.itemMainBinding.infoContainer.setOnClickListener { - setSelectServer(guid) - } - } -// if (holder is FooterViewHolder) { -// if (true) { -// holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE -// } else { -// holder.itemFooterBinding.layoutEdit.setOnClickListener { -// Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}") -// } -// } -// } - } - - /** - * Gets the server address information - * Hides part of IP or domain information for privacy protection - * @param profile The server configuration - * @return Formatted address string - */ - private fun getAddress(profile: ProfileItem): String { - // Hide xxx:xxx:***/xxx.xxx.xxx.*** - return "${ - profile.server?.let { - if (it.contains(":")) - it.split(":").take(2).joinToString(":", postfix = ":***") - else - it.split('.').dropLast(1).joinToString(".", postfix = ".***") - } - } : ${profile.serverPort}" - } - - /** - * Gets the subscription remarks information - * @param profile The server configuration - * @return Subscription remarks string, or empty string if none - */ - private fun getSubscriptionRemarks(profile: ProfileItem): String { - val subRemarks = - if (mActivity.mainViewModel.subscriptionId.isEmpty()) - MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks?.firstOrNull() - else - null - return subRemarks?.toString() ?: "" - } - - /** - * Shares server configuration - * Displays a dialog with sharing options and executes the selected action - * @param guid The server unique identifier - * @param profile The server configuration - * @param position The position in the list - * @param shareOptions The list of share options - * @param skip The number of options to skip - */ - private fun shareServer(guid: String, profile: ProfileItem, position: Int, shareOptions: List, skip: Int) { - AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i -> - try { - when (i + skip) { - 0 -> showQRCode(guid) - 1 -> share2Clipboard(guid) - 2 -> shareFullContent(guid) - 3 -> editServer(guid, profile) - 4 -> removeServer(guid, position) - else -> mActivity.toast("else") - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Error when sharing server", e) - } - }.show() - } - - /** - * Displays QR code for the server configuration - * @param guid The server unique identifier - */ - private fun showQRCode(guid: String) { - val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity)) - ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid)) - AlertDialog.Builder(mActivity).setView(ivBinding.root).show() - } - - /** - * Shares server configuration to clipboard - * @param guid The server unique identifier - */ - private fun share2Clipboard(guid: String) { - if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) { - mActivity.toastSuccess(R.string.toast_success) - } else { - mActivity.toastError(R.string.toast_failure) - } - } - - /** - * Shares full server configuration content to clipboard - * @param guid The server unique identifier - */ - private fun shareFullContent(guid: String) { - mActivity.lifecycleScope.launch(Dispatchers.IO) { - val result = AngConfigManager.shareFullContent2Clipboard(mActivity, guid) - launch(Dispatchers.Main) { - if (result == 0) { - mActivity.toastSuccess(R.string.toast_success) - } else { - mActivity.toastError(R.string.toast_failure) - } - } - } - } - - /** - * Edits server configuration - * Opens appropriate editing interface based on configuration type - * @param guid The server unique identifier - * @param profile The server configuration - */ - private fun editServer(guid: String, profile: ProfileItem) { - val intent = Intent().putExtra("guid", guid) - .putExtra("isRunning", isRunning) - .putExtra("createConfigType", profile.configType.value) - if (profile.configType == EConfigType.CUSTOM) { - mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java)) - } else { - mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java)) - } - } - - /** - * Removes server configuration - * Handles confirmation dialog and related checks - * @param guid The server unique identifier - * @param position The position in the list - */ - private fun removeServer(guid: String, position: Int) { - if (guid != MmkvManager.getSelectServer()) { - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { - AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm) - .setPositiveButton(android.R.string.ok) { _, _ -> - removeServerSub(guid, position) - } - .setNegativeButton(android.R.string.cancel) { _, _ -> - //do noting - } - .show() - } else { - removeServerSub(guid, position) - } - } else { - application.toast(R.string.toast_action_not_allowed) - } - } - - /** - * Executes the actual server removal process - * @param guid The server unique identifier - * @param position The position in the list - */ - private fun removeServerSub(guid: String, position: Int) { - mActivity.mainViewModel.removeServer(guid) - notifyItemRemoved(position) - notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size) - } - - /** - * Sets the selected server - * Updates UI and restarts service if needed - * @param guid The server unique identifier to select - */ - private fun setSelectServer(guid: String) { - val selected = MmkvManager.getSelectServer() - if (guid != selected) { - MmkvManager.setSelectServer(guid) - if (!TextUtils.isEmpty(selected)) { - notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty())) - } - notifyItemChanged(mActivity.mainViewModel.getPosition(guid)) - if (isRunning) { - V2RayServiceManager.stopVService(mActivity) - mActivity.lifecycleScope.launch { - try { - delay(500) - V2RayServiceManager.startVService(mActivity) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to restart V2Ray service", e) - } - } - } - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { - return when (viewType) { - VIEW_TYPE_ITEM -> - MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - - else -> - FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - } - } - - override fun getItemViewType(position: Int): Int { - return if (position == mActivity.mainViewModel.serversCache.size) { - VIEW_TYPE_FOOTER - } else { - VIEW_TYPE_ITEM - } - } - - open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - fun onItemSelected() { - itemView.setBackgroundColor(Color.LTGRAY) - } - - fun onItemClear() { - itemView.setBackgroundColor(0) - } - } - - class MainViewHolder(val itemMainBinding: ItemRecyclerMainBinding) : - BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder - - class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) : - BaseViewHolder(itemFooterBinding.root) - - override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { - mActivity.mainViewModel.swapServer(fromPosition, toPosition) - notifyItemMoved(fromPosition, toPosition) - return true - } - - override fun onItemMoveCompleted() { - // do nothing - } - - override fun onItemDismiss(position: Int) { - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt deleted file mode 100644 index b0df22e5..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt +++ /dev/null @@ -1,279 +0,0 @@ -package com.v2ray.ang.ui - -import android.annotation.SuppressLint -import android.os.Bundle -import android.text.TextUtils -import android.util.Log -import android.view.Menu -import android.view.MenuItem -import android.widget.Toast -import androidx.appcompat.widget.SearchView -import androidx.lifecycle.lifecycleScope -import com.v2ray.ang.AppConfig -import com.v2ray.ang.AppConfig.ANG_PACKAGE -import com.v2ray.ang.R -import com.v2ray.ang.databinding.ActivityBypassListBinding -import com.v2ray.ang.dto.AppInfo -import com.v2ray.ang.extension.toast -import com.v2ray.ang.extension.toastSuccess -import com.v2ray.ang.extension.v2RayApplication -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.handler.SettingsManager -import com.v2ray.ang.util.AppManagerUtil -import com.v2ray.ang.util.HttpUtil -import com.v2ray.ang.util.Utils -import es.dmoral.toasty.Toasty -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.text.Collator - -class PerAppProxyActivity : BaseActivity() { - private val binding by lazy { ActivityBypassListBinding.inflate(layoutInflater) } - - private var adapter: PerAppProxyAdapter? = null - private var appsAll: List? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - - title = getString(R.string.per_app_proxy_settings) - - addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) - - lifecycleScope.launch { - try { - binding.pbWaiting.show() - val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET) - val apps = withContext(Dispatchers.IO) { - val appsList = AppManagerUtil.loadNetworkAppList(this@PerAppProxyActivity) - - if (blacklist != null) { - appsList.forEach { app -> - app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0 - } - appsList.sortedWith { p1, p2 -> - when { - p1.isSelected > p2.isSelected -> -1 - p1.isSelected == p2.isSelected -> 0 - else -> 1 - } - } - } else { - val collator = Collator.getInstance() - appsList.sortedWith(compareBy(collator) { it.appName }) - } - } - - appsAll = apps - adapter = PerAppProxyAdapter(this@PerAppProxyActivity, apps, blacklist) - binding.recyclerView.adapter = adapter - binding.pbWaiting.hide() - } catch (e: Exception) { - binding.pbWaiting.hide() - Log.e(ANG_PACKAGE, "Error loading apps", e) - } - } - - binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked -> - MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked) - } - binding.switchPerAppProxy.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false) - - binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked -> - MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked) - } - binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false) - - binding.layoutSwitchBypassAppsTips.setOnClickListener { - Toasty.info(this, R.string.summary_pref_per_app_proxy, Toast.LENGTH_LONG, true).show() - } - } - - override fun onPause() { - super.onPause() - adapter?.let { - MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist) - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_bypass_list, 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 { - filterProxyApp(newText.orEmpty()) - return false - } - }) - } - - - return super.onCreateOptionsMenu(menu) - } - - - @SuppressLint("NotifyDataSetChanged") - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.select_all -> adapter?.let { it -> - val pkgNames = it.apps.map { it.packageName } - if (it.blacklist.containsAll(pkgNames)) { - it.apps.forEach { - val packageName = it.packageName - adapter?.blacklist?.remove(packageName) - } - } else { - it.apps.forEach { - val packageName = it.packageName - adapter?.blacklist?.add(packageName) - } - } - it.notifyDataSetChanged() - true - } == true - - R.id.select_proxy_app -> { - selectProxyApp() - true - } - - R.id.import_proxy_app -> { - importProxyApp() - true - } - - R.id.export_proxy_app -> { - exportProxyApp() - true - } - - else -> super.onOptionsItemSelected(item) - } - - private fun selectProxyApp() { - toast(R.string.msg_downloading_content) - binding.pbWaiting.show() - - val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL - lifecycleScope.launch(Dispatchers.IO) { - var content = HttpUtil.getUrlContent(url, 5000) - if (content.isNullOrEmpty()) { - val httpPort = SettingsManager.getHttpPort() - content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: "" - } - launch(Dispatchers.Main) { - Log.i(AppConfig.TAG, content) - selectProxyApp(content, true) - toastSuccess(R.string.toast_success) - binding.pbWaiting.hide() - } - } - } - - private fun importProxyApp() { - val content = Utils.getClipboard(applicationContext) - if (TextUtils.isEmpty(content)) return - selectProxyApp(content, false) - toastSuccess(R.string.toast_success) - } - - private fun exportProxyApp() { - var lst = binding.switchBypassApps.isChecked.toString() - - adapter?.blacklist?.forEach block@{ - lst = lst + System.getProperty("line.separator") + it - } - Utils.setClipboard(applicationContext, lst) - toastSuccess(R.string.toast_success) - } - - @SuppressLint("NotifyDataSetChanged") - private fun selectProxyApp(content: String, force: Boolean): Boolean { - try { - val proxyApps = if (TextUtils.isEmpty(content)) { - Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt") - } else { - content - } - if (TextUtils.isEmpty(proxyApps)) return false - - adapter?.blacklist?.clear() - - if (binding.switchBypassApps.isChecked) { - adapter?.let { it -> - it.apps.forEach block@{ - val packageName = it.packageName - Log.i(AppConfig.TAG, packageName) - if (!inProxyApps(proxyApps, packageName, force)) { - adapter?.blacklist?.add(packageName) - println(packageName) - return@block - } - } - it.notifyDataSetChanged() - } - } else { - adapter?.let { it -> - it.apps.forEach block@{ - val packageName = it.packageName - Log.i(AppConfig.TAG, packageName) - if (inProxyApps(proxyApps, packageName, force)) { - adapter?.blacklist?.add(packageName) - println(packageName) - return@block - } - } - it.notifyDataSetChanged() - } - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Error selecting proxy app", e) - return false - } - return true - } - - private fun inProxyApps(proxyApps: String, packageName: String, force: Boolean): Boolean { - if (force) { - if (packageName == "com.google.android.webview") return false - if (packageName.startsWith("com.google")) return true - } - - return proxyApps.indexOf(packageName) >= 0 - } - - private fun filterProxyApp(content: String): Boolean { - val apps = ArrayList() - - val key = content.uppercase() - if (key.isNotEmpty()) { - appsAll?.forEach { - if (it.appName.uppercase().indexOf(key) >= 0 - || it.packageName.uppercase().indexOf(key) >= 0 - ) { - apps.add(it) - } - } - } else { - appsAll?.forEach { - apps.add(it) - } - } - - adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist) - binding.recyclerView.adapter = adapter - refreshData() - return true - } - - @SuppressLint("NotifyDataSetChanged") - fun refreshData() { - adapter?.notifyDataSetChanged() - } -} 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 deleted file mode 100644 index bb0ee572..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt +++ /dev/null @@ -1,132 +0,0 @@ -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.extension.toastSuccess -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) - toastSuccess(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 deleted file mode 100644 index e585cff8..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.v2ray.ang.ui - -import android.Manifest -import android.annotation.SuppressLint -import android.content.Intent -import android.os.Bundle -import android.util.Log -import android.view.Menu -import android.view.MenuItem -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.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.extension.toastError -import com.v2ray.ang.extension.toastSuccess -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) - } - - private val requestCameraPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java)) - } else { - toast(R.string.toast_permission_denied) - } - } - - 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) - addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) - binding.recyclerView.adapter = adapter - - mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) - mItemTouchHelper?.attachToRecyclerView(binding.recyclerView) - - binding.tvDomainStrategySummary.text = getDomainStrategy() - binding.layoutDomainStrategy.setOnClickListener { - setDomainStrategy() - } - } - - 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): Boolean = when (item.itemId) { - R.id.add_rule -> startActivity(Intent(this, RoutingEditActivity::class.java)).let { true } - R.id.import_predefined_rulesets -> importPredefined().let { true } - R.id.import_rulesets_from_clipboard -> importFromClipboard().let { true } - R.id.import_rulesets_from_qrcode -> requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA).let { true } - R.id.export_rulesets_to_clipboard -> export2Clipboard().let { true } - else -> super.onOptionsItemSelected(item) - } - - private fun getDomainStrategy(): String { - return MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: routing_domain_strategy.first() - } - - private fun setDomainStrategy() { - android.app.AlertDialog.Builder(this).setItems(routing_domain_strategy.asList().toTypedArray()) { _, i -> - try { - val value = routing_domain_strategy[i] - MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, value) - binding.tvDomainStrategySummary.text = value - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to set domain strategy", e) - } - }.show() - } - - private fun importPredefined() { - AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i -> - AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip) - .setPositiveButton(android.R.string.ok) { _, _ -> - try { - lifecycleScope.launch(Dispatchers.IO) { - SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i) - launch(Dispatchers.Main) { - refreshData() - toastSuccess(R.string.toast_success) - } - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to import predefined ruleset", e) - } - } - .setNegativeButton(android.R.string.cancel) { _, _ -> - //do nothing - } - .show() - }.show() - } - - private fun importFromClipboard() { - 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) { - Log.e(AppConfig.TAG, "Failed to get clipboard content", e) - toastError(R.string.toast_failure) - return@setPositiveButton - } - lifecycleScope.launch(Dispatchers.IO) { - val result = SettingsManager.resetRoutingRulesets(clipboard) - withContext(Dispatchers.Main) { - if (result) { - refreshData() - toastSuccess(R.string.toast_success) - } else { - toastError(R.string.toast_failure) - } - } - } - } - .setNegativeButton(android.R.string.cancel) { _, _ -> - //do nothing - } - .show() - } - - private fun export2Clipboard() { - val rulesetList = MmkvManager.decodeRoutingRulesets() - if (rulesetList.isNullOrEmpty()) { - toastError(R.string.toast_failure) - } else { - Utils.setClipboard(this, JsonUtil.toJson(rulesetList)) - toastSuccess(R.string.toast_success) - } - } - - 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() - toastSuccess(R.string.toast_success) - } else { - toastError(R.string.toast_failure) - } - } - } - } - .setNegativeButton(android.R.string.cancel) { _, _ -> - //do nothing - } - .show() - return true - } - - @SuppressLint("NotifyDataSetChanged") - fun refreshData() { - rulesets.clear() - rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf()) - adapter.notifyDataSetChanged() - } -} \ No newline at end of file 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 deleted file mode 100644 index bd48ea59..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingRecyclerAdapter.kt +++ /dev/null @@ -1,80 +0,0 @@ -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/java/com/v2ray/ang/ui/ScScannerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt deleted file mode 100644 index 9a972f93..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.v2ray.ang.ui - -import android.Manifest -import android.content.Intent -import android.os.Bundle -import androidx.activity.result.contract.ActivityResultContracts -import com.v2ray.ang.R -import com.v2ray.ang.extension.toast -import com.v2ray.ang.extension.toastError -import com.v2ray.ang.extension.toastSuccess -import com.v2ray.ang.handler.AngConfigManager - -class ScScannerActivity : BaseActivity() { - - private val requestCameraPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - scanQRCode.launch(Intent(this, ScannerActivity::class.java)) - } else { - toast(R.string.toast_permission_denied) - finish() - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_none) - importQRcode() - } - - private fun importQRcode(): Boolean { - requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA) - return true - } - - private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty() - val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false) - - if (count + countSub > 0) { - toastSuccess(R.string.toast_success) - } else { - toastError(R.string.toast_failure) - } - - startActivity(Intent(this, MainActivity::class.java)) - } - finish() - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt deleted file mode 100644 index 2a971ac0..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.v2ray.ang.ui - -import android.Manifest -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.BitmapFactory -import android.os.Build -import android.os.Bundle -import android.util.Log -import android.view.Menu -import android.view.MenuItem -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.extension.toast -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.util.QRCodeDecoder -import io.github.g00fy2.quickie.QRResult -import io.github.g00fy2.quickie.ScanCustomCode -import io.github.g00fy2.quickie.config.ScannerConfig - -class ScannerActivity : BaseActivity() { - - - private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult) - private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - val uri = it.data?.data - if (it.resultCode == RESULT_OK && uri != null) { - try { - val inputStream = contentResolver.openInputStream(uri) - val bitmap = BitmapFactory.decodeStream(inputStream) - inputStream?.close() - - val text = QRCodeDecoder.syncDecodeQRCode(bitmap) - if (text.isNullOrEmpty()) { - toast(R.string.toast_decoding_failed) - } else { - finished(text) - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to decode QR code from file", e) - toast(R.string.toast_decoding_failed) - } - } - } - - private val requestPermissionLauncher = - registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - showFileChooser() - } else { - toast(R.string.toast_permission_denied) - } - } - - public override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) { - launchScan() - } - } - - private fun launchScan() { - scanQrCode.launch( - ScannerConfig.build { - setHapticSuccessFeedback(true) // enable (default) or disable haptic feedback when a barcode was detected - setShowTorchToggle(true) // show or hide (default) torch/flashlight toggle button - setShowCloseButton(true) // show or hide (default) close button - } - ) - } - - private fun handleResult(result: QRResult) { - if (result is QRResult.QRSuccess) { - finished(result.content.rawValue.orEmpty()) - } else { - finish() - } - } - - private fun finished(text: String) { - val intent = Intent() - intent.putExtra("SCAN_RESULT", text) - setResult(RESULT_OK, intent) - finish() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_scanner, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.scan_code -> { - launchScan() - true - } - - R.id.select_photo -> { - val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Manifest.permission.READ_MEDIA_IMAGES - } else { - Manifest.permission.READ_EXTERNAL_STORAGE - } - - if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { - showFileChooser() - } else { - requestPermissionLauncher.launch(permission) - } - true - } - - - else -> super.onOptionsItemSelected(item) - } - - private fun showFileChooser() { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.type = "image/*" - intent.addCategory(Intent.CATEGORY_OPENABLE) - //intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) - - try { - chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) - } catch (ex: android.content.ActivityNotFoundException) { - toast(R.string.toast_require_file_manager) - } - } -} \ No newline at end of file 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 deleted file mode 100644 index e9bdad66..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt +++ /dev/null @@ -1,652 +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.v2ray.ang.AppConfig -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_MTU -import com.v2ray.ang.R -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.NetworkType -import com.v2ray.ang.dto.ProfileItem -import com.v2ray.ang.extension.isNotNullEmpty -import com.v2ray.ang.extension.toast -import com.v2ray.ang.extension.toastSuccess -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) } - - - 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 - ) - et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU) - } else if (config.configType == EConfigType.HYSTERIA2) { - et_obfs_password?.text = Utils.getEditable(config.obfsPassword) - et_port_hop?.text = Utils.getEditable(config.portHopping) - et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval) - et_pinsha256?.text = Utils.getEditable(config.pinSHA256) - 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 { it -> - val utlsIndex = Utils.arrayFind(uTlsItems, it) - utlsIndex.let { sp_stream_fingerprint?.setSelection(if (it >= 0) it else 0) } - } - config.alpn?.let { it -> - val alpnIndex = Utils.arrayFind(alpns, it) - alpnIndex.let { sp_stream_alpn?.setSelection(if (it >= 0) it else 0) } - } - 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) - 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.i(AppConfig.TAG, JsonUtil.toJsonPretty(config) ?: "") - MmkvManager.encodeServerConfig(editGuid, config) - toastSuccess(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()) - } 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 - } - - 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/java/com/v2ray/ang/ui/SettingsActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt deleted file mode 100644 index 6af64e3a..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt +++ /dev/null @@ -1,372 +0,0 @@ -package com.v2ray.ang.ui - -import android.content.Intent -import android.os.Bundle -import android.text.TextUtils -import android.view.View -import androidx.activity.viewModels -import androidx.preference.CheckBoxPreference -import androidx.preference.EditTextPreference -import androidx.preference.ListPreference -import androidx.preference.PreferenceFragmentCompat -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.PeriodicWorkRequest -import androidx.work.multiprocess.RemoteWorkManager -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.Utils -import com.v2ray.ang.viewmodel.SettingsViewModel -import java.util.concurrent.TimeUnit - -class SettingsActivity : BaseActivity() { - private val settingsViewModel: SettingsViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_settings) - - title = getString(R.string.title_settings) - - settingsViewModel.startListenPreferenceChange() - } - - class SettingsFragment : PreferenceFragmentCompat() { - - 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 vpnBypassLan by lazy { findPreference(AppConfig.PREF_VPN_BYPASS_LAN) } - private val vpnInterfaceAddress by lazy { findPreference(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX) } - - private val mux by lazy { findPreference(AppConfig.PREF_MUX_ENABLED) } - private val muxConcurrency by lazy { findPreference(AppConfig.PREF_MUX_CONCURRENCY) } - private val muxXudpConcurrency by lazy { findPreference(AppConfig.PREF_MUX_XUDP_CONCURRENCY) } - private val muxXudpQuic by lazy { findPreference(AppConfig.PREF_MUX_XUDP_QUIC) } - - private val fragment by lazy { findPreference(AppConfig.PREF_FRAGMENT_ENABLED) } - private val fragmentPackets by lazy { findPreference(AppConfig.PREF_FRAGMENT_PACKETS) } - private val fragmentLength by lazy { findPreference(AppConfig.PREF_FRAGMENT_LENGTH) } - private val fragmentInterval by lazy { findPreference(AppConfig.PREF_FRAGMENT_INTERVAL) } - - private val autoUpdateCheck by lazy { findPreference(AppConfig.SUBSCRIPTION_AUTO_UPDATE) } - private val autoUpdateInterval by lazy { findPreference(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) } - - private val socksPort by lazy { findPreference(AppConfig.PREF_SOCKS_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) } - - override fun onCreatePreferences(bundle: Bundle?, s: String?) { - addPreferencesFromResource(R.xml.pref_settings) - - perAppProxy?.setOnPreferenceClickListener { - startActivity(Intent(activity, PerAppProxyActivity::class.java)) - perAppProxy?.isChecked = true - false - } - localDns?.setOnPreferenceChangeListener { _, any -> - updateLocalDns(any as Boolean) - true - } - localDnsPort?.setOnPreferenceChangeListener { _, any -> - val nval = any as String - localDnsPort?.summary = - if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval - true - } - vpnDns?.setOnPreferenceChangeListener { _, any -> - vpnDns?.summary = any as String - true - } - - mux?.setOnPreferenceChangeListener { _, newValue -> - updateMux(newValue as Boolean) - true - } - muxConcurrency?.setOnPreferenceChangeListener { _, newValue -> - updateMuxConcurrency(newValue as String) - true - } - muxXudpConcurrency?.setOnPreferenceChangeListener { _, newValue -> - updateMuxXudpConcurrency(newValue as String) - true - } - - fragment?.setOnPreferenceChangeListener { _, newValue -> - updateFragment(newValue as Boolean) - true - } - fragmentPackets?.setOnPreferenceChangeListener { _, newValue -> - updateFragmentPackets(newValue as String) - true - } - fragmentLength?.setOnPreferenceChangeListener { _, newValue -> - updateFragmentLength(newValue as String) - true - } - fragmentInterval?.setOnPreferenceChangeListener { _, newValue -> - updateFragmentInterval(newValue as String) - true - } - - autoUpdateCheck?.setOnPreferenceChangeListener { _, newValue -> - val value = newValue as Boolean - autoUpdateCheck?.isChecked = value - autoUpdateInterval?.isEnabled = value - autoUpdateInterval?.text?.toLongEx()?.let { - if (newValue) configureUpdateTask(it) else cancelUpdateTask() - } - true - } - autoUpdateInterval?.setOnPreferenceChangeListener { _, any -> - var nval = any as String - - // It must be greater than 15 minutes because WorkManager couldn't run tasks under 15 minutes intervals - nval = - if (TextUtils.isEmpty(nval) || nval.toLongEx() < 15) AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL else nval - autoUpdateInterval?.summary = nval - configureUpdateTask(nval.toLongEx()) - true - } - - socksPort?.setOnPreferenceChangeListener { _, any -> - val nval = any as String - socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval - true - } - - remoteDns?.setOnPreferenceChangeListener { _, any -> - val nval = any as String - remoteDns?.summary = if (nval == "") AppConfig.DNS_PROXY else nval - true - } - domesticDns?.setOnPreferenceChangeListener { _, any -> - val nval = any as String - 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.DELAY_TEST_URL else nval - true - } - mode?.setOnPreferenceChangeListener { _, newValue -> - updateMode(newValue.toString()) - true - } - mode?.dialogLayoutResource = R.layout.preference_with_help_link - //loglevel.summary = "LogLevel" - - } - - override fun onStart() { - super.onStart() - 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, "50-100") - fragmentInterval?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20") - - autoUpdateCheck?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) - autoUpdateInterval?.summary = - MmkvManager.decodeSettingsString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL) - autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) - - socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS) - remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY) - domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT) - dnsHosts?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS) - delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DELAY_TEST_URL) - - initSharedPreference() - } - - private fun initSharedPreference() { - listOf( - localDnsPort, - vpnDns, - muxConcurrency, - muxXudpConcurrency, - fragmentLength, - fragmentInterval, - autoUpdateInterval, - socksPort, - remoteDns, - domesticDns, - delayTestUrl - ).forEach { key -> - key?.text = key?.summary.toString() - } - - listOf( - AppConfig.PREF_SNIFFING_ENABLED, - ).forEach { key -> - findPreference(key)?.isChecked = - MmkvManager.decodeSettingsBool(key, true) - } - - listOf( - AppConfig.PREF_ROUTE_ONLY_ENABLED, - AppConfig.PREF_IS_BOOTED, - AppConfig.PREF_BYPASS_APPS, - AppConfig.PREF_SPEED_ENABLED, - AppConfig.PREF_CONFIRM_REMOVE, - AppConfig.PREF_START_SCAN_IMMEDIATE, - AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, - AppConfig.PREF_PREFER_IPV6, - AppConfig.PREF_PROXY_SHARING, - AppConfig.PREF_ALLOW_INSECURE - ).forEach { key -> - findPreference(key)?.isChecked = - MmkvManager.decodeSettingsBool(key, false) - } - - listOf( - AppConfig.PREF_VPN_BYPASS_LAN, - AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, - AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, - AppConfig.PREF_MUX_XUDP_QUIC, - AppConfig.PREF_FRAGMENT_PACKETS, - AppConfig.PREF_LANGUAGE, - AppConfig.PREF_UI_MODE_NIGHT, - AppConfig.PREF_LOGLEVEL, - AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, - AppConfig.PREF_MODE - ).forEach { key -> - if (MmkvManager.decodeSettingsString(key) != null) { - findPreference(key)?.value = MmkvManager.decodeSettingsString(key) - } - } - } - - private fun updateMode(mode: String?) { - val vpn = mode == VPN - perAppProxy?.isEnabled = vpn - perAppProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false) - localDns?.isEnabled = vpn - fakeDns?.isEnabled = vpn - appendHttpProxy?.isEnabled = vpn - localDnsPort?.isEnabled = vpn - vpnDns?.isEnabled = vpn - vpnBypassLan?.isEnabled = vpn - vpnInterfaceAddress?.isEnabled = vpn - if (vpn) { - updateLocalDns( - MmkvManager.decodeSettingsBool( - AppConfig.PREF_LOCAL_DNS_ENABLED, - false - ) - ) - } - } - - private fun updateLocalDns(enabled: Boolean) { - fakeDns?.isEnabled = enabled - localDnsPort?.isEnabled = enabled - vpnDns?.isEnabled = !enabled - } - - private fun configureUpdateTask(interval: Long) { - val rw = RemoteWorkManager.getInstance(AngApplication.application) - rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME) - rw.enqueueUniquePeriodicWork( - AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME, - ExistingPeriodicWorkPolicy.UPDATE, - PeriodicWorkRequest.Builder( - SubscriptionUpdater.UpdateTask::class.java, - interval, - TimeUnit.MINUTES - ) - .apply { - setInitialDelay(interval, TimeUnit.MINUTES) - } - .build() - ) - } - - private fun cancelUpdateTask() { - val rw = RemoteWorkManager.getInstance(AngApplication.application) - rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME) - } - - private fun updateMux(enabled: Boolean) { - muxConcurrency?.isEnabled = enabled - muxXudpConcurrency?.isEnabled = enabled - muxXudpQuic?.isEnabled = enabled - if (enabled) { - updateMuxConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8")) - updateMuxXudpConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")) - } - } - - private fun updateMuxConcurrency(value: String?) { - val concurrency = value?.toIntOrNull() ?: 8 - muxConcurrency?.summary = concurrency.toString() - } - - - private fun updateMuxXudpConcurrency(value: String?) { - if (value == null) { - muxXudpQuic?.isEnabled = true - } else { - val concurrency = value.toIntOrNull() ?: 8 - muxXudpConcurrency?.summary = concurrency.toString() - muxXudpQuic?.isEnabled = concurrency >= 0 - } - } - - private fun updateFragment(enabled: Boolean) { - fragmentPackets?.isEnabled = enabled - fragmentLength?.isEnabled = enabled - fragmentInterval?.isEnabled = enabled - if (enabled) { - updateFragmentPackets(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")) - updateFragmentLength(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")) - updateFragmentInterval(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")) - } - } - - private fun updateFragmentPackets(value: String?) { - fragmentPackets?.summary = value.toString() - } - - private fun updateFragmentLength(value: String?) { - fragmentLength?.summary = value.toString() - } - - private fun updateFragmentInterval(value: String?) { - fragmentInterval?.summary = value.toString() - } - } - - fun onModeHelpClicked(view: View) { - Utils.openUri(this, AppConfig.APP_WIKI_MODE) - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt deleted file mode 100644 index f85382f1..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt +++ /dev/null @@ -1,165 +0,0 @@ -package com.v2ray.ang.ui - -import android.os.Bundle -import android.text.TextUtils -import android.view.Menu -import android.view.MenuItem -import androidx.appcompat.app.AlertDialog -import androidx.lifecycle.lifecycleScope -import com.v2ray.ang.AppConfig -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.extension.toastSuccess -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.util.Utils -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -class SubEditActivity : BaseActivity() { - private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) } - - private var del_config: MenuItem? = null - private var save_config: MenuItem? = null - - private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - title = getString(R.string.title_sub_setting) - - val subItem = MmkvManager.decodeSubscription(editSubId) - if (subItem != null) { - bindingServer(subItem) - } else { - clearServer() - } - } - - /** - * binding selected server config - */ - 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.allowInsecureUrl.isChecked = subItem.allowInsecureUrl - binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile) - binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile) - return true - } - - /** - * clear or init server config - */ - 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 - } - - /** - * save server config - */ - private fun saveServer(): Boolean { - 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() - subItem.allowInsecureUrl = binding.allowInsecureUrl.isChecked - - if (TextUtils.isEmpty(subItem.remarks)) { - toast(R.string.sub_setting_remarks) - 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) - if (!subItem.allowInsecureUrl) { - return false - } - } - } - - MmkvManager.encodeSubscription(editSubId, subItem) - toastSuccess(R.string.toast_success) - finish() - return true - } - - /** - * save server config - */ - private fun deleteServer(): Boolean { - if (editSubId.isNotEmpty()) { - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { - AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) - .setPositiveButton(android.R.string.ok) { _, _ -> - lifecycleScope.launch(Dispatchers.IO) { - MmkvManager.removeSubscription(editSubId) - launch(Dispatchers.Main) { - finish() - } - } - } - .setNegativeButton(android.R.string.cancel) { _, _ -> - // do nothing - } - .show() - } else { - lifecycleScope.launch(Dispatchers.IO) { - MmkvManager.removeSubscription(editSubId) - launch(Dispatchers.Main) { - finish() - } - } - } - } - return true - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.action_server, menu) - del_config = menu.findItem(R.id.del_config) - save_config = menu.findItem(R.id.save_config) - - if (editSubId.isEmpty()) { - 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/SubSettingActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt deleted file mode 100644 index ef27fcff..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.v2ray.ang.ui - -import android.annotation.SuppressLint -import android.content.Intent -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -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.dto.SubscriptionItem -import com.v2ray.ang.extension.toastError -import com.v2ray.ang.extension.toastSuccess -import com.v2ray.ang.handler.AngConfigManager -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.helper.SimpleItemTouchHelperCallback -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -class SubSettingActivity : BaseActivity() { - private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) } - - var subscriptions: List> = listOf() - private val adapter by lazy { SubSettingRecyclerAdapter(this) } - private var mItemTouchHelper: ItemTouchHelper? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - - title = getString(R.string.title_sub_setting) - - binding.recyclerView.setHasFixedSize(true) - binding.recyclerView.layoutManager = LinearLayoutManager(this) - addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) - binding.recyclerView.adapter = adapter - - mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) - mItemTouchHelper?.attachToRecyclerView(binding.recyclerView) - } - - override fun onResume() { - super.onResume() - refreshData() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.action_sub_setting, menu) - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.add_config -> { - startActivity(Intent(this, SubEditActivity::class.java)) - true - } - - R.id.sub_update -> { - binding.pbWaiting.show() - - lifecycleScope.launch(Dispatchers.IO) { - val count = AngConfigManager.updateConfigViaSubAll() - delay(500L) - launch(Dispatchers.Main) { - if (count > 0) { - toastSuccess(R.string.toast_success) - } else { - toastError(R.string.toast_failure) - } - binding.pbWaiting.hide() - } - } - - true - } - - else -> super.onOptionsItemSelected(item) - - } - - @SuppressLint("NotifyDataSetChanged") - fun refreshData() { - subscriptions = MmkvManager.decodeSubscriptions() - adapter.notifyDataSetChanged() - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt deleted file mode 100644 index cc2d5404..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt +++ /dev/null @@ -1,163 +0,0 @@ -package com.v2ray.ang.ui - -import android.content.Intent -import android.graphics.Color -import android.text.TextUtils -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AlertDialog -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.RecyclerView -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.databinding.ItemQrcodeBinding -import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding -import com.v2ray.ang.extension.toast -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.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 -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter(), ItemTouchHelperAdapter { - - private var mActivity: SubSettingActivity = activity - - private val share_method: Array by lazy { - mActivity.resources.getStringArray(R.array.share_sub_method) - } - - override fun getItemCount() = mActivity.subscriptions.size - - override fun onBindViewHolder(holder: MainViewHolder, position: Int) { - val subId = mActivity.subscriptions[position].first - val subItem = mActivity.subscriptions[position].second - holder.itemSubSettingBinding.tvName.text = subItem.remarks - holder.itemSubSettingBinding.tvUrl.text = subItem.url - holder.itemSubSettingBinding.chkEnable.isChecked = subItem.enabled - holder.itemView.setBackgroundColor(Color.TRANSPARENT) - - holder.itemSubSettingBinding.layoutEdit.setOnClickListener { - mActivity.startActivity( - Intent(mActivity, SubEditActivity::class.java) - .putExtra("subId", subId) - ) - } - - holder.itemSubSettingBinding.layoutRemove.setOnClickListener { - removeSubscription(subId, position) - } - - holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked -> - if (!it.isPressed) return@setOnCheckedChangeListener - subItem.enabled = isChecked - MmkvManager.encodeSubscription(subId, subItem) - - } - - if (TextUtils.isEmpty(subItem.url)) { - holder.itemSubSettingBinding.layoutUrl.visibility = View.GONE - holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE - holder.itemSubSettingBinding.chkEnable.visibility = View.INVISIBLE - } else { - holder.itemSubSettingBinding.layoutUrl.visibility = View.VISIBLE - holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE - holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE - holder.itemSubSettingBinding.layoutShare.setOnClickListener { - AlertDialog.Builder(mActivity) - .setItems(share_method.asList().toTypedArray()) { _, i -> - try { - when (i) { - 0 -> { - val ivBinding = - ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity)) - ivBinding.ivQcode.setImageBitmap( - QRCodeDecoder.createQRCode( - subItem.url - - ) - ) - AlertDialog.Builder(mActivity).setView(ivBinding.root).show() - } - - 1 -> { - Utils.setClipboard(mActivity, subItem.url) - } - - else -> mActivity.toast("else") - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Share subscription failed", e) - } - }.show() - } - } - } - - private fun removeSubscription(subId: String, position: Int) { - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { - AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm) - .setPositiveButton(android.R.string.ok) { _, _ -> - removeSubscriptionSub(subId, position) - } - .setNegativeButton(android.R.string.cancel) { _, _ -> - //do noting - } - .show() - } else { - removeSubscriptionSub(subId, position) - } - } - - private fun removeSubscriptionSub(subId: String, position: Int) { - mActivity.lifecycleScope.launch(Dispatchers.IO) { - MmkvManager.removeSubscription(subId) - launch(Dispatchers.Main) { - notifyItemRemoved(position) - notifyItemRangeChanged(position, mActivity.subscriptions.size) - mActivity.refreshData() - } - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder { - return MainViewHolder( - ItemRecyclerSubSettingBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - - class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) : - 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/java/com/v2ray/ang/ui/UrlSchemeActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt deleted file mode 100644 index 364f442a..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.v2ray.ang.ui - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.util.Log -import androidx.lifecycle.lifecycleScope -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.databinding.ActivityLogcatBinding -import com.v2ray.ang.extension.toast -import com.v2ray.ang.extension.toastError -import com.v2ray.ang.handler.AngConfigManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.net.URLDecoder - -class UrlSchemeActivity : BaseActivity() { - private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - - try { - intent.apply { - if (action == Intent.ACTION_SEND) { - if ("text/plain" == type) { - intent.getStringExtra(Intent.EXTRA_TEXT)?.let { - parseUri(it, null) - } - } - } else if (action == Intent.ACTION_VIEW) { - when (data?.host) { - "install-config" -> { - val uri: Uri? = intent.data - val shareUrl = uri?.getQueryParameter("url").orEmpty() - parseUri(shareUrl, uri?.fragment) - } - - "install-sub" -> { - val uri: Uri? = intent.data - val shareUrl = uri?.getQueryParameter("url").orEmpty() - parseUri(shareUrl, uri?.fragment) - } - - else -> { - toastError(R.string.toast_failure) - } - } - } - } - - startActivity(Intent(this, MainActivity::class.java)) - finish() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Error processing URL scheme", e) - } - } - - private fun parseUri(uriString: String?, fragment: String?) { - if (uriString.isNullOrEmpty()) { - return - } - Log.i(AppConfig.TAG, uriString) - - var decodedUrl = URLDecoder.decode(uriString, "UTF-8") - val uri = Uri.parse(decodedUrl) - if (uri != null) { - if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) { - decodedUrl += "#${fragment}" - } - Log.i(AppConfig.TAG, decodedUrl) - lifecycleScope.launch(Dispatchers.IO) { - val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false) - withContext(Dispatchers.Main) { - if (count + countSub > 0) { - toast(R.string.import_subscription_success) - } else { - toast(R.string.import_subscription_failure) - } - } - } - } - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt deleted file mode 100644 index efc0a39c..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt +++ /dev/null @@ -1,376 +0,0 @@ -package com.v2ray.ang.ui - -import android.Manifest -import android.annotation.SuppressLint -import android.app.AlertDialog -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.provider.OpenableColumns -import android.util.Log -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuItem -import android.view.View.GONE -import android.view.View.VISIBLE -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.databinding.ActivityUserAssetBinding -import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding -import com.v2ray.ang.dto.AssetUrlItem -import com.v2ray.ang.extension.concatUrl -import com.v2ray.ang.extension.toTrafficString -import com.v2ray.ang.extension.toast -import com.v2ray.ang.extension.toastError -import com.v2ray.ang.extension.toastSuccess -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.handler.SettingsManager -import com.v2ray.ang.util.HttpUtil -import com.v2ray.ang.util.Utils -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileOutputStream -import java.net.HttpURLConnection -import java.text.DateFormat -import java.util.Date - -class UserAssetActivity : BaseActivity() { - private val binding by lazy { ActivityUserAssetBinding.inflate(layoutInflater) } - - val extDir by lazy { File(Utils.userAssetPath(this)) } - val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat") - - private val requestStoragePermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.type = "*/*" - intent.addCategory(Intent.CATEGORY_OPENABLE) - - try { - chooseFile.launch( - Intent.createChooser( - intent, - getString(R.string.title_file_chooser) - ) - ) - } catch (ex: android.content.ActivityNotFoundException) { - toast(R.string.toast_require_file_manager) - } - } else { - toast(R.string.toast_permission_denied) - } - } - - private val requestCameraPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java)) - } else { - toast(R.string.toast_permission_denied) - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - title = getString(R.string.title_user_asset_setting) - - binding.recyclerView.setHasFixedSize(true) - binding.recyclerView.layoutManager = LinearLayoutManager(this) - addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) - binding.recyclerView.adapter = UserAssetAdapter() - - binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources() - binding.layoutGeoFilesSources.setOnClickListener { - setGeoFilesSources() - } - } - - override fun onResume() { - super.onResume() - refreshData() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_asset, menu) - return super.onCreateOptionsMenu(menu) - } - - // 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) - } - - private fun getGeoFilesSources(): String { - return MmkvManager.decodeSettingsString(AppConfig.PREF_GEO_FILES_SOURCES) ?: AppConfig.GEO_FILES_SOURCES.first() - } - - private fun setGeoFilesSources() { - AlertDialog.Builder(this).setItems(AppConfig.GEO_FILES_SOURCES.toTypedArray()) { _, i -> - try { - val value = AppConfig.GEO_FILES_SOURCES[i] - MmkvManager.encodeSettings(AppConfig.PREF_GEO_FILES_SOURCES, value) - binding.tvGeoFilesSourcesSummary.text = value - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to set geo files sources", e) - } - }.show() - } - - private fun showFileChooser() { - val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Manifest.permission.READ_MEDIA_IMAGES - } else { - Manifest.permission.READ_EXTERNAL_STORAGE - } - requestStoragePermissionLauncher.launch(permission) - } - - private 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" - ) - - 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) - } - }.onFailure { - toastError(R.string.toast_asset_copy_failed) - MmkvManager.removeAssetUrl(assetId) - } - } - } - - private fun copyFile(uri: Uri): String { - val targetFile = File(extDir, getCursorName(uri) ?: uri.toString()) - contentResolver.openInputStream(uri).use { inputStream -> - targetFile.outputStream().use { fileOut -> - inputStream?.copyTo(fileOut) - toastSuccess(R.string.toast_success) - refreshData() - } - } - return targetFile.path - } - - private fun getCursorName(uri: Uri): String? = try { - contentResolver.query(uri, null, null, null, null)?.let { cursor -> - cursor.run { - if (moveToFirst()) getString(getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) - else null - }.also { cursor.close() } - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to get cursor name", e) - null - } - - private fun importAssetFromQRcode(): Boolean { - requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA) - 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) { - Log.e(AppConfig.TAG, "Failed to import asset from URL", e) - return false - } - return true - } - - private fun downloadGeoFiles() { - binding.pbWaiting.show() - toast(R.string.msg_downloading_content) - - val httpPort = SettingsManager.getHttpPort() - var assets = MmkvManager.decodeAssetUrls() - assets = addBuiltInGeoItems(assets) - - var resultCount = 0 - lifecycleScope.launch(Dispatchers.IO) { - assets.forEach { - try { - var result = downloadGeo(it.second, 15000, httpPort) - if (!result) { - result = downloadGeo(it.second, 15000, 0) - } - if (result) - resultCount++ - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to download geo file: ${it.second.remarks}", e) - } - } - withContext(Dispatchers.Main) { - if (resultCount > 0) { - toast(getString(R.string.title_update_config_count, resultCount)) - refreshData() - } else { - toast(getString(R.string.toast_failure)) - } - binding.pbWaiting.hide() - } - } - } - - private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean { - val targetTemp = File(extDir, item.remarks + "_temp") - val target = File(extDir, item.remarks) - Log.i(AppConfig.TAG, "Downloading geo file: ${item.remarks} from ${item.url}") - - val conn = HttpUtil.createProxyConnection(item.url, httpPort, timeout, timeout, needStream = true) ?: return false - try { - val inputStream = conn.inputStream - val responseCode = conn.responseCode - if (responseCode == HttpURLConnection.HTTP_OK) { - FileOutputStream(targetTemp).use { output -> - inputStream.copyTo(output) - } - - targetTemp.renameTo(target) - } - return true - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e) - return false - } finally { - conn.disconnect() - } - } - - private fun addBuiltInGeoItems(assets: List>): List> { - val list = mutableListOf>() - builtInGeoFiles - .filter { geoFile -> assets.none { it.second.remarks == geoFile } } - .forEach { - list.add( - Utils.getUuid() to AssetUrlItem( - it, - String.format(AppConfig.GITHUB_DOWNLOAD_URL, getGeoFilesSources()).concatUrl(it), - locked = true - ) - ) - } - - return list + assets - } - - fun initAssets() { - lifecycleScope.launch(Dispatchers.Default) { - SettingsManager.initAssets(this@UserAssetActivity, assets) - withContext(Dispatchers.Main) { - refreshData() - } - } - } - - @SuppressLint("NotifyDataSetChanged") - fun refreshData() { - binding.recyclerView.adapter?.notifyDataSetChanged() - } - - inner class UserAssetAdapter : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder { - return UserAssetViewHolder( - ItemRecyclerUserAssetBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - - @SuppressLint("SetTextI18n") - override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) { - var assets = MmkvManager.decodeAssetUrls() - assets = addBuiltInGeoItems(assets) - val item = assets.getOrNull(position) ?: return -// file with name == item.second.remarks - val file = extDir.listFiles()?.find { it.name == item.second.remarks } - - holder.itemUserAssetBinding.assetName.text = item.second.remarks - - if (file != null) { - val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM) - holder.itemUserAssetBinding.assetProperties.text = - "${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}" - } else { - holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found) - } - - if (item.second.locked == true) { - holder.itemUserAssetBinding.layoutEdit.visibility = GONE - //holder.itemUserAssetBinding.layoutRemove.visibility = GONE - } else { - holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE } - //holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE - } - - holder.itemUserAssetBinding.layoutEdit.setOnClickListener { - val intent = Intent(this@UserAssetActivity, UserAssetUrlActivity::class.java) - intent.putExtra("assetId", item.first) - startActivity(intent) - } - holder.itemUserAssetBinding.layoutRemove.setOnClickListener { - AlertDialog.Builder(this@UserAssetActivity).setMessage(R.string.del_config_comfirm) - .setPositiveButton(android.R.string.ok) { _, _ -> - file?.delete() - MmkvManager.removeAssetUrl(item.first) - initAssets() - } - .setNegativeButton(android.R.string.cancel) { _, _ -> - //do noting - } - .show() - } - } - - override fun getItemCount(): Int { - var assets = MmkvManager.decodeAssetUrls() - assets = addBuiltInGeoItems(assets) - return assets.size - } - } - - class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) : - RecyclerView.ViewHolder(itemUserAssetBinding.root) -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt deleted file mode 100644 index 3d390967..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt +++ /dev/null @@ -1,160 +0,0 @@ -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 androidx.appcompat.app.AlertDialog -import com.v2ray.ang.AppConfig -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.extension.toastSuccess -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) } - - private var del_config: MenuItem? = null - private var save_config: MenuItem? = null - - private val extDir by lazy { File(Utils.userAssetPath(this)) } - private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - title = getString(R.string.title_user_asset_add_url) - - 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() - } - } - - /** - * bingding seleced asset config - */ - private fun bindingAsset(assetItem: AssetUrlItem): Boolean { - binding.etRemarks.text = Utils.getEditable(assetItem.remarks) - binding.etUrl.text = Utils.getEditable(assetItem.url) - return true - } - - /** - * clear or init asset config - */ - private fun clearAsset(): Boolean { - binding.etRemarks.text = null - binding.etUrl.text = null - return true - } - - /** - * save asset config - */ - private fun saveServer(): Boolean { - var assetItem = MmkvManager.decodeAsset(editAssetId) - var assetId = editAssetId - if (assetItem != null) { - // remove file associated with the asset - val file = extDir.resolve(assetItem.remarks) - if (file.exists()) { - try { - file.delete() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to delete asset file: ${file.path}", e) - } - } - } else { - assetId = Utils.getUuid() - assetItem = AssetUrlItem() - } - - assetItem.remarks = binding.etRemarks.text.toString() - assetItem.url = binding.etUrl.text.toString() - - // 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 false - } - - - if (TextUtils.isEmpty(assetItem.remarks)) { - toast(R.string.sub_setting_remarks) - return false - } - if (TextUtils.isEmpty(assetItem.url)) { - toast(R.string.title_url) - return false - } - - MmkvManager.encodeAsset(assetId, assetItem) - toastSuccess(R.string.toast_success) - finish() - return true - } - - /** - * save server config - */ - private fun deleteServer(): Boolean { - if (editAssetId.isNotEmpty()) { - AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) - .setPositiveButton(android.R.string.ok) { _, _ -> - MmkvManager.removeAssetUrl(editAssetId) - finish() - } - .setNegativeButton(android.R.string.cancel) { _, _ -> - // do nothing - } - .show() - } - return true - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.action_server, menu) - del_config = menu.findItem(R.id.del_config) - save_config = menu.findItem(R.id.save_config) - - if (editAssetId.isEmpty()) { - 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) - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt deleted file mode 100644 index 577698ea..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.v2ray.ang.util - -import android.content.Context -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import com.v2ray.ang.dto.AppInfo -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -object AppManagerUtil { - /** - * Load the list of network applications. - * - * @param context The context to use. - * @return A list of AppInfo objects representing the network applications. - */ - suspend fun loadNetworkAppList(context: Context): ArrayList = - withContext(Dispatchers.IO) { - val packageManager = context.packageManager - val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS) - val apps = ArrayList() - - for (pkg in packages) { - val applicationInfo = pkg.applicationInfo ?: continue - - val appName = applicationInfo.loadLabel(packageManager).toString() - 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) - } - - return@withContext apps - } - - fun getLastUpdateTime(context: Context): Long = - context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime - -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt deleted file mode 100644 index 7172728e..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt +++ /dev/null @@ -1,223 +0,0 @@ -package com.v2ray.ang.util - -import android.util.Log -import com.v2ray.ang.AppConfig -import com.v2ray.ang.AppConfig.LOOPBACK -import com.v2ray.ang.BuildConfig -import com.v2ray.ang.util.Utils.encode -import com.v2ray.ang.util.Utils.urlDecode -import java.io.IOException -import java.net.HttpURLConnection -import java.net.IDN -import java.net.Inet6Address -import java.net.InetAddress -import java.net.InetSocketAddress -import java.net.Proxy -import java.net.URL - -object HttpUtil { - - /** - * Converts the domain part of a URL string to its IDN (Punycode, ASCII Compatible Encoding) format. - * - * For example, a URL like "https://例子.中国/path" will be converted to "https://xn--fsqu00a.xn--fiqs8s/path". - * - * @param str The URL string to convert (can contain non-ASCII characters in the domain). - * @return The URL string with the domain part converted to ASCII-compatible (Punycode) format. - */ - fun toIdnUrl(str: String): String { - val url = URL(str) - val host = url.host - val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED) - if (host != asciiHost) { - return str.replace(host, asciiHost) - } else { - return str - } - } - - /** - * Converts a Unicode domain name to its IDN (Punycode, ASCII Compatible Encoding) format. - * If the input is an IP address or already an ASCII domain, returns the original string. - * - * @param domain The domain string to convert (can include non-ASCII internationalized characters). - * @return The domain in ASCII-compatible (Punycode) format, or the original string if input is an IP or already ASCII. - */ - fun toIdnDomain(domain: String): String { - // Return as is if it's a pure IP address (IPv4 or IPv6) - if (Utils.isPureIpAddress(domain)) { - return domain - } - - // Return as is if already ASCII (English domain or already punycode) - if (domain.all { it.code < 128 }) { - return domain - } - - // Otherwise, convert to ASCII using IDN - return IDN.toASCII(domain, IDN.ALLOW_UNASSIGNED) - } - - /** - * Resolves a hostname to an IP address, returns original input if it's already an IP - * - * @param host The hostname or IP address to resolve - * @param ipv6Preferred Whether to prefer IPv6 addresses, defaults to false - * @return The resolved IP address or the original input (if it's already an IP or resolution fails) - */ - fun resolveHostToIP(host: String, ipv6Preferred: Boolean = false): List? { - try { - // If it's already an IP address, return it as a list - if (Utils.isPureIpAddress(host)) { - return null - } - - // Get all IP addresses - val addresses = InetAddress.getAllByName(host) - if (addresses.isEmpty()) { - return null - } - - // Sort addresses based on preference - val sortedAddresses = if (ipv6Preferred) { - addresses.sortedWith(compareByDescending { it is Inet6Address }) - } else { - addresses.sortedWith(compareBy { it is Inet6Address }) - } - - val ipList = sortedAddresses.mapNotNull { it.hostAddress } - - Log.i(AppConfig.TAG, "Resolved IPs for $host: ${ipList.joinToString()}") - - return ipList - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to resolve host to IP", e) - return null - } - } - - - /** - * Retrieves the content of a URL as a string. - * - * @param url The URL to fetch content from. - * @param timeout The timeout value in milliseconds. - * @param httpPort The HTTP port to use. - * @return The content of the URL as a string. - */ - fun getUrlContent(url: String, timeout: Int, httpPort: Int = 0): String? { - val conn = createProxyConnection(url, httpPort, timeout, timeout) ?: return null - try { - return conn.inputStream.bufferedReader().readText() - } catch (_: Exception) { - } finally { - conn.disconnect() - } - return null - } - - /** - * Retrieves the content of a URL as a string with a custom User-Agent header. - * - * @param url The URL to fetch content from. - * @param timeout The timeout value in milliseconds. - * @param httpPort The HTTP port to use. - * @return The content of the URL as a string. - * @throws IOException If an I/O error occurs. - */ - @Throws(IOException::class) - fun getUrlContentWithUserAgent(url: String?, timeout: Int = 15000, httpPort: Int = 0): String { - var currentUrl = url - var redirects = 0 - val maxRedirects = 3 - - while (redirects++ < maxRedirects) { - if (currentUrl == null) continue - val conn = createProxyConnection(currentUrl, httpPort, timeout, timeout) ?: continue - conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}") - conn.connect() - - val responseCode = conn.responseCode - when (responseCode) { - in 300..399 -> { - val location = conn.getHeaderField("Location") - conn.disconnect() - if (location.isNullOrEmpty()) { - throw IOException("Redirect location not found") - } - currentUrl = location - continue - } - - else -> try { - return conn.inputStream.use { it.bufferedReader().readText() } - } finally { - conn.disconnect() - } - } - } - throw IOException("Too many redirects") - } - - /** - * Creates an HttpURLConnection object connected through a proxy. - * - * @param urlStr The target URL address. - * @param port The port of the proxy server. - * @param connectTimeout The connection timeout in milliseconds (default is 15000 ms). - * @param readTimeout The read timeout in milliseconds (default is 15000 ms). - * @param needStream Whether the connection needs to support streaming. - * @return Returns a configured HttpURLConnection object, or null if it fails. - */ - fun createProxyConnection( - urlStr: String, - port: Int, - connectTimeout: Int = 15000, - readTimeout: Int = 15000, - needStream: Boolean = false - ): HttpURLConnection? { - - var conn: HttpURLConnection? = null - try { - val url = URL(urlStr) - // Create a connection - conn = if (port == 0) { - url.openConnection() - } else { - url.openConnection( - Proxy( - Proxy.Type.HTTP, - InetSocketAddress(LOOPBACK, port) - ) - ) - } as HttpURLConnection - - // Set connection and read timeouts - conn.connectTimeout = connectTimeout - conn.readTimeout = readTimeout - if (!needStream) { - // Set request headers - conn.setRequestProperty("Connection", "close") - // Disable automatic redirects - conn.instanceFollowRedirects = false - // Disable caching - conn.useCaches = false - } - - //Add Basic Authorization - url.userInfo?.let { - conn.setRequestProperty( - "Authorization", - "Basic ${encode(urlDecode(it))}" - ) - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to create proxy connection", e) - // If an exception occurs, close the connection and return null - conn?.disconnect() - return null - } - return conn - } -} - 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 deleted file mode 100644 index 80a40fbf..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/JsonUtil.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.v2ray.ang.util - -import android.util.Log -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 com.v2ray.ang.AppConfig -import java.lang.reflect.Type - -object JsonUtil { - private var gson = Gson() - - /** - * Converts an object to its JSON representation. - * - * @param src The object to convert. - * @return The JSON representation of the object. - */ - fun toJson(src: Any?): String { - return gson.toJson(src) - } - - /** - * Parses a JSON string into an object of the specified class. - * - * @param src The JSON string to parse. - * @param cls The class of the object to parse into. - * @return The parsed object. - */ - fun fromJson(src: String, cls: Class): T { - return gson.fromJson(src, cls) - } - - /** - * Converts an object to its pretty-printed JSON representation. - * - * @param src The object to convert. - * @return The pretty-printed JSON representation of the object, or null if the object is null. - */ - 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) - } - - /** - * Parses a JSON string into a JsonObject. - * - * @param src The JSON string to parse. - * @return The parsed JsonObject, or null if parsing fails. - */ - fun parseString(src: String?): JsonObject? { - if (src == null) - return null - try { - return JsonParser.parseString(src).getAsJsonObject() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to parse JSON string", e) - return null - } - } -} \ No newline at end of file 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 deleted file mode 100644 index 2b9f71aa..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.v2ray.ang.util - -import android.content.Context -import android.os.SystemClock -import android.util.Log -import com.v2ray.ang.AppConfig -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ProfileItem -import com.v2ray.ang.fmt.Hysteria2Fmt -import com.v2ray.ang.handler.SpeedtestManager -import com.v2ray.ang.service.ProcessService -import java.io.File - -object PluginUtil { - private const val HYSTERIA2 = "libhysteria2.so" - - private val procService: ProcessService by lazy { - ProcessService() - } - - /** - * Run the plugin based on the provided configuration. - * - * @param context The context to use. - * @param config The profile configuration. - * @param socksPort The port information. - */ - fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) { - Log.i(AppConfig.TAG, "Starting plugin execution") - - if (config == null) { - Log.w(AppConfig.TAG, "Cannot run plugin: config is null") - return - } - - try { - if (config.configType == EConfigType.HYSTERIA2) { - if (socksPort == null) { - Log.w(AppConfig.TAG, "Cannot run plugin: socksPort is null") - return - } - Log.i(AppConfig.TAG, "Running Hysteria2 plugin") - val configFile = genConfigHy2(context, config, socksPort) ?: return - val cmd = genCmdHy2(context, configFile) - - procService.runProcess(context, cmd) - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Error running plugin", e) - } - } - - /** - * Stop the running plugin. - */ - fun stopPlugin() { - stopHy2() - } - - /** - * Perform a real ping using Hysteria2. - * - * @param context The context to use. - * @param config The profile configuration. - * @return The ping delay in milliseconds, or -1 if it fails. - */ - fun realPingHy2(context: Context, config: ProfileItem?): Long { - Log.i(AppConfig.TAG, "realPingHy2") - val retFailure = -1L - - if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) { - val socksPort = Utils.findFreePort(listOf(0)) - val configFile = genConfigHy2(context, config, socksPort) ?: return retFailure - val cmd = genCmdHy2(context, configFile) - - val proc = ProcessService() - proc.runProcess(context, cmd) - Thread.sleep(1000L) - val delay = SpeedtestManager.testConnection(context, socksPort) - proc.stopProcess() - - return delay.first - } - return retFailure - } - - /** - * Generate the configuration file for Hysteria2. - * - * @param context The context to use. - * @param config The profile configuration. - * @param socksPort The port information. - * @return The generated configuration file. - */ - private fun genConfigHy2(context: Context, config: ProfileItem, socksPort: Int): File? { - Log.i(AppConfig.TAG, "runPlugin $HYSTERIA2") - - val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null - - val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json") - Log.i(AppConfig.TAG, "runPlugin ${configFile.absolutePath}") - - configFile.parentFile?.mkdirs() - configFile.writeText(JsonUtil.toJson(hy2Config)) - Log.i(AppConfig.TAG, JsonUtil.toJson(hy2Config)) - - return configFile - } - - /** - * Generate the command to run Hysteria2. - * - * @param context The context to use. - * @param configFile The configuration file. - * @return The command to run Hysteria2. - */ - private fun genCmdHy2(context: Context, configFile: File): MutableList { - return mutableListOf( - File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath, - "--disable-update-check", - "--config", - configFile.absolutePath, - "--log-level", - "warn", - "client" - ) - } - - /** - * Stop the Hysteria2 process. - */ - private fun stopHy2() { - try { - Log.i(AppConfig.TAG, "$HYSTERIA2 destroy") - procService?.stopProcess() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to stop Hysteria2 process", e) - } - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.kt deleted file mode 100644 index 446739b6..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.kt +++ /dev/null @@ -1,123 +0,0 @@ -package com.v2ray.ang.util - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import com.google.zxing.BarcodeFormat -import com.google.zxing.BinaryBitmap -import com.google.zxing.DecodeHintType -import com.google.zxing.EncodeHintType -import com.google.zxing.NotFoundException -import com.google.zxing.RGBLuminanceSource -import com.google.zxing.common.GlobalHistogramBinarizer -import com.google.zxing.qrcode.QRCodeReader -import com.google.zxing.qrcode.QRCodeWriter -import java.util.EnumMap - -/** - * QR code decoder utility. - */ -object QRCodeDecoder { - val HINTS: MutableMap = EnumMap(DecodeHintType::class.java) - - /** - * Creates a QR code bitmap from the given text. - * - * @param text The text to encode in the QR code. - * @param size The size of the QR code bitmap. - * @return The generated QR code bitmap, or null if an error occurs. - */ - fun createQRCode(text: String, size: Int = 800): Bitmap? { - return runCatching { - val hints = mapOf(EncodeHintType.CHARACTER_SET to Charsets.UTF_8) - 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() - } - Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply { - setPixels(pixels, 0, size, 0, 0, size, size) - } - }.getOrNull() - } - - /** - * Decodes a QR code from a local image file. This method is time-consuming and should be called in a background thread. - * - * @param picturePath The local path of the image file to decode. - * @return The content of the QR code, or null if decoding fails. - */ - fun syncDecodeQRCode(picturePath: String): String? { - return syncDecodeQRCode(getDecodeAbleBitmap(picturePath)) - } - - /** - * Decodes a QR code from a bitmap. This method is time-consuming and should be called in a background thread. - * - * @param bitmap The bitmap to decode. - * @return The content of the QR code, or null if decoding fails. - */ - fun syncDecodeQRCode(bitmap: Bitmap?): String? { - 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)).text - } - }.getOrNull() - } - } - - /** - * Converts a local image file to a bitmap that can be decoded as a QR code. The image is compressed to avoid being too large. - * - * @param picturePath The local path of the image file. - * @return The decoded bitmap, or null if an error occurs. - */ - private fun getDecodeAbleBitmap(picturePath: String): Bitmap? { - return try { - val options = BitmapFactory.Options() - options.inJustDecodeBounds = true - BitmapFactory.decodeFile(picturePath, options) - var sampleSize = options.outHeight / 400 - if (sampleSize <= 0) { - sampleSize = 1 - } - options.inSampleSize = sampleSize - options.inJustDecodeBounds = false - BitmapFactory.decodeFile(picturePath, options) - } catch (e: Exception) { - null - } - } - - init { - val allFormats: List = arrayListOf( - BarcodeFormat.AZTEC, - BarcodeFormat.CODABAR, - BarcodeFormat.CODE_39, - BarcodeFormat.CODE_93, - BarcodeFormat.CODE_128, - BarcodeFormat.DATA_MATRIX, - BarcodeFormat.EAN_8, - BarcodeFormat.EAN_13, - BarcodeFormat.ITF, - BarcodeFormat.MAXICODE, - BarcodeFormat.PDF_417, - BarcodeFormat.QR_CODE, - BarcodeFormat.RSS_14, - BarcodeFormat.RSS_EXPANDED, - BarcodeFormat.UPC_A, - BarcodeFormat.UPC_E, - BarcodeFormat.UPC_EAN_EXTENSION - ) - HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE - HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats - HINTS[DecodeHintType.CHARACTER_SET] = Charsets.UTF_8 - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt deleted file mode 100644 index 148ce4ec..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt +++ /dev/null @@ -1,570 +0,0 @@ -package com.v2ray.ang.util - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.content.res.Configuration.UI_MODE_NIGHT_MASK -import android.content.res.Configuration.UI_MODE_NIGHT_NO -import android.os.Build -import android.os.LocaleList -import android.provider.Settings -import android.text.Editable -import android.util.Base64 -import android.util.Log -import android.util.Patterns -import android.webkit.URLUtil -import androidx.core.content.ContextCompat -import androidx.core.net.toUri -import com.v2ray.ang.AppConfig -import com.v2ray.ang.AppConfig.LOOPBACK -import com.v2ray.ang.BuildConfig -import java.io.IOException -import java.net.InetAddress -import java.net.ServerSocket -import java.net.URI -import java.net.URLDecoder -import java.net.URLEncoder -import java.util.Locale -import java.util.UUID - -object Utils { - - private val IPV4_REGEX = - Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$") - private val IPV6_REGEX = Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$") - - /** - * Convert string to editable for Kotlin. - * - * @param text The string to convert. - * @return An Editable instance containing the text. - */ - fun getEditable(text: String?): Editable { - return Editable.Factory.getInstance().newEditable(text.orEmpty()) - } - - /** - * Find the position of a value in an array. - * - * @param array The array to search. - * @param value The value to find. - * @return The index of the value in the array, or -1 if not found. - */ - fun arrayFind(array: Array, value: String): Int { - return array.indexOf(value) - } - - /** - * Parse a string to an integer with a default value. - * - * @param str The string to parse. - * @param default The default value if parsing fails. - * @return The parsed integer, or the default value if parsing fails. - */ - fun parseInt(str: String?, default: Int = 0): Int { - return str?.toIntOrNull() ?: default - } - - /** - * Get text from the clipboard. - * - * @param context The context to use. - * @return The text from the clipboard, or an empty string if an error occurs. - */ - fun getClipboard(context: Context): String { - return try { - val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - cmb.primaryClip?.getItemAt(0)?.text.toString() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to get clipboard content", e) - "" - } - } - - /** - * Set text to the clipboard. - * - * @param context The context to use. - * @param content The text to set to the clipboard. - */ - fun setClipboard(context: Context, content: String) { - try { - val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clipData = ClipData.newPlainText(null, content) - cmb.setPrimaryClip(clipData) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to set clipboard content", e) - } - } - - /** - * Decode a base64 encoded string. - * - * @param text The base64 encoded string. - * @return The decoded string, or an empty string if decoding fails. - */ - fun decode(text: String?): String { - return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) }.orEmpty() - } - - /** - * Try to decode a base64 encoded string. - * - * @param text The base64 encoded string. - * @return The decoded string, or null if decoding fails. - */ - fun tryDecodeBase64(text: String?): String? { - if (text.isNullOrEmpty()) return null - - try { - return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to decode standard base64", e) - } - try { - return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to decode URL-safe base64", e) - } - return null - } - - /** - * Encode a string to base64. - * - * @param text The string to encode. - * @return The base64 encoded string, or an empty string if encoding fails. - */ - fun encode(text: String): String { - return try { - Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to encode text to base64", e) - "" - } - } - - /** - * Check if a string is a valid IP address. - * - * @param value The string to check. - * @return True if the string is a valid IP address, false otherwise. - */ - fun isIpAddress(value: String?): Boolean { - if (value.isNullOrEmpty()) return false - - try { - var addr = value.trim() - if (addr.isEmpty()) return false - - //CIDR - if (addr.contains("/")) { - val arr = addr.split("/") - if (arr.size == 2 && arr[1].toIntOrNull() != null && arr[1].toInt() > -1) { - addr = arr[0] - } - } - - // Handle IPv4-mapped IPv6 addresses - if (addr.startsWith("::ffff:") && '.' in addr) { - addr = addr.drop(7) - } else if (addr.startsWith("[::ffff:") && '.' in addr) { - addr = addr.drop(8).replace("]", "") - } - - val octets = addr.split('.') - if (octets.size == 4) { - if (octets[3].contains(":")) { - addr = addr.substring(0, addr.indexOf(":")) - } - return isIpv4Address(addr) - } - - return isIpv6Address(addr) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to validate IP address", e) - return false - } - } - - /** - * Check if a string is a pure IP address (IPv4 or IPv6). - * - * @param value The string to check. - * @return True if the string is a pure IP address, false otherwise. - */ - fun isPureIpAddress(value: String): Boolean { - return isIpv4Address(value) || isIpv6Address(value) - } - - /** - * Check if a string is a valid domain name. - * - * A valid domain name must not be an IP address and must be a valid URL format. - * - * @param input The string to check. - * @return True if the string is a valid domain name, false otherwise. - */ - fun isDomainName(input: String?): Boolean { - if (input.isNullOrEmpty()) return false - - // Must not be an IP address and must be a valid URL format - return !isPureIpAddress(input) && isValidUrl(input) - } - - /** - * Check if a string is a valid IPv4 address. - * - * @param value The string to check. - * @return True if the string is a valid IPv4 address, false otherwise. - */ - private fun isIpv4Address(value: String): Boolean { - return IPV4_REGEX.matches(value) - } - - /** - * Check if a string is a valid IPv6 address. - * - * @param value The string to check. - * @return True if the string is a valid IPv6 address, false otherwise. - */ - private fun isIpv6Address(value: String): Boolean { - var addr = value - if (addr.startsWith("[") && addr.endsWith("]")) { - addr = addr.drop(1).dropLast(1) - } - return IPV6_REGEX.matches(addr) - } - - /** - * Check if a string is a CoreDNS address. - * - * @param s The string to check. - * @return True if the string is a CoreDNS address, false otherwise. - */ - fun isCoreDNSAddress(s: String): Boolean { - return s.startsWith("https") || - s.startsWith("tcp") || - s.startsWith("quic") || - s == "localhost" - } - - /** - * Check if a string is a valid URL. - * - * @param value The string to check. - * @return True if the string is a valid URL, false otherwise. - */ - fun isValidUrl(value: String?): Boolean { - if (value.isNullOrEmpty()) return false - - return try { - Patterns.WEB_URL.matcher(value).matches() || - Patterns.DOMAIN_NAME.matcher(value).matches() || - URLUtil.isValidUrl(value) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to validate URL", e) - false - } - } - - /** - * Open a URI in a browser. - * - * @param context The context to use. - * @param uriString The URI string to open. - */ - fun openUri(context: Context, uriString: String) { - try { - val uri = uriString.toUri() - context.startActivity(Intent(Intent.ACTION_VIEW, uri)) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to open URI", e) - } - } - - /** - * Generate a UUID. - * - * @return A UUID string without dashes. - */ - fun getUuid(): String { - return try { - UUID.randomUUID().toString().replace("-", "") - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to generate UUID", e) - "" - } - } - - /** - * Decode a URL-encoded string. - * - * @param url The URL-encoded string. - * @return The decoded string, or the original string if decoding fails. - */ - fun urlDecode(url: String): String { - return try { - URLDecoder.decode(url, Charsets.UTF_8.toString()) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to decode URL", e) - url - } - } - - /** - * Encode a string to URL-encoded format. - * - * @param url The string to encode. - * @return The URL-encoded string, or the original string if encoding fails. - */ - fun urlEncode(url: String): String { - return try { - URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20") - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to encode URL", e) - url - } - } - - /** - * Read text from an asset file. - * - * @param context The context to use. - * @param fileName The name of the asset file. - * @return The content of the asset file as a string. - */ - fun readTextFromAssets(context: Context?, fileName: String): String { - if (context == null) return "" - - return try { - context.assets.open(fileName).use { inputStream -> - inputStream.bufferedReader().use { reader -> - reader.readText() - } - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to read asset file: $fileName", e) - "" - } - } - - /** - * Get the path to the user asset directory. - * - * @param context The context to use. - * @return The path to the user asset directory. - */ - fun userAssetPath(context: Context?): String { - if (context == null) return "" - - return try { - context.getExternalFilesDir(AppConfig.DIR_ASSETS)?.absolutePath - ?: context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to get user asset path", e) - "" - } - } - - /** - * Get the path to the backup directory. - * - * @param context The context to use. - * @return The path to the backup directory. - */ - fun backupPath(context: Context?): String { - if (context == null) return "" - - return try { - context.getExternalFilesDir(AppConfig.DIR_BACKUPS)?.absolutePath - ?: context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to get backup path", e) - "" - } - } - - /** - * Get the device ID for XUDP base key. - * - * @return The device ID for XUDP base key. - */ - fun getDeviceIdForXUDPBaseKey(): String { - return try { - val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8) - Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE)) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to generate device ID", e) - "" - } - } - - /** - * Get the dark mode status. - * - * @param context The context to use. - * @return True if dark mode is enabled, false otherwise. - */ - fun getDarkModeStatus(context: Context): Boolean { - return context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK != UI_MODE_NIGHT_NO - } - - /** - * Get the IPv6 address in a formatted string. - * - * @param address The IPv6 address. - * @return The formatted IPv6 address, or the original address if not valid. - */ - fun getIpv6Address(address: String?): String { - if (address.isNullOrEmpty()) return "" - - return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) { - "[$address]" - } else { - address - } - } - - /** - * Get the system locale. - * - * @return The system locale. - */ - fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - LocaleList.getDefault()[0] - } else { - Locale.getDefault() - } - - /** - * Fix illegal characters in a URL. - * - * @param str The URL string. - * @return The URL string with illegal characters replaced. - */ - fun fixIllegalUrl(str: String): String { - return str.replace(" ", "%20") - .replace("|", "%7C") - } - - /** - * Find a free port from a list of ports. - * - * @param ports The list of ports to check. - * @return The first free port found. - * @throws IOException If no free port is found. - */ - 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") - } - - /** - * Check if a string is a valid subscription URL. - * - * @param value The string to check. - * @return True if the string is a valid subscription URL, false otherwise. - */ - fun isValidSubUrl(value: String?): Boolean { - if (value.isNullOrEmpty()) return false - - try { - if (URLUtil.isHttpsUrl(value)) return true - if (URLUtil.isHttpUrl(value)) { - if (value.contains(LOOPBACK)) return true - - //Check private ip address - val uri = URI(fixIllegalUrl(value)) - if (isIpAddress(uri.host)) { - AppConfig.PRIVATE_IP_LIST.forEach { - if (isIpInCidr(uri.host, it)) return true - } - } - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to validate subscription URL", e) - } - return false - } - - /** - * Get the receiver flags based on the Android version. - * - * @return The receiver flags. - */ - fun receiverFlags(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - ContextCompat.RECEIVER_EXPORTED - } else { - ContextCompat.RECEIVER_NOT_EXPORTED - } - - /** - * Check if the package is Xray. - * - * @return True if the package is Xray, false otherwise. - */ - fun isXray(): Boolean = BuildConfig.APPLICATION_ID.startsWith("com.v2ray.ang") - - /** - * Check if it is the Google Play version. - * - * @return True if the package is Google Play, false otherwise. - */ - fun isGoogleFlavor(): Boolean = BuildConfig.FLAVOR == "playstore" - - /** - * Converts an InetAddress to its long representation - * - * @param ip The InetAddress to convert - * @return The long representation of the IP address - */ - private fun inetAddressToLong(ip: InetAddress): Long { - val bytes = ip.address - var result: Long = 0 - for (i in bytes.indices) { - result = result shl 8 or (bytes[i].toInt() and 0xff).toLong() - } - return result - } - - /** - * Check if an IP address is within a CIDR range - * - * @param ip The IP address to check - * @param cidr The CIDR notation range (e.g., "192.168.1.0/24") - * @return True if the IP is within the CIDR range, false otherwise - */ - fun isIpInCidr(ip: String, cidr: String): Boolean { - try { - if (!isIpAddress(ip)) return false - - // Parse CIDR (e.g., "192.168.1.0/24") - val (cidrIp, prefixLen) = cidr.split("/") - val prefixLength = prefixLen.toInt() - - // Convert IP and CIDR's IP portion to Long - val ipLong = inetAddressToLong(InetAddress.getByName(ip)) - val cidrIpLong = inetAddressToLong(InetAddress.getByName(cidrIp)) - - // Calculate subnet mask (e.g., /24 → 0xFFFFFF00) - val mask = if (prefixLength == 0) 0L else (-1L shl (32 - prefixLength)) - - // Check if they're in the same subnet - return (ipLong and mask) == (cidrIpLong and mask) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to check if IP is in CIDR", e) - return false - } - } -} - diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/ZipUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/ZipUtil.kt deleted file mode 100644 index 9d9dce62..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/ZipUtil.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.v2ray.ang.util - -import android.util.Log -import com.v2ray.ang.AppConfig -import java.io.BufferedOutputStream -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream -import java.util.zip.ZipEntry -import java.util.zip.ZipFile -import java.util.zip.ZipOutputStream - -object ZipUtil { - private const val BUFFER_SIZE = 4096 - - /** - * Zip the contents of a folder. - * - * @param folderPath The path to the folder to zip. - * @param outputZipFilePath The path to the output zip file. - * @return True if the operation is successful, false otherwise. - * @throws IOException If an I/O error occurs. - */ - @Throws(IOException::class) - fun zipFromFolder(folderPath: String, outputZipFilePath: String): Boolean { - val buffer = ByteArray(BUFFER_SIZE) - - try { - if (folderPath.isEmpty() || outputZipFilePath.isEmpty()) { - return false - } - - val filesToCompress = ArrayList() - val directory = File(folderPath) - if (directory.isDirectory) { - directory.listFiles()?.forEach { - if (it.isFile) { - filesToCompress.add(it.absolutePath) - } - } - } - if (filesToCompress.isEmpty()) { - return false - } - - val zos = ZipOutputStream(FileOutputStream(outputZipFilePath)) - - filesToCompress.forEach { file -> - val ze = ZipEntry(File(file).name) - zos.putNextEntry(ze) - val inputStream = FileInputStream(file) - while (true) { - val len = inputStream.read(buffer) - if (len <= 0) break - zos.write(buffer, 0, len) - } - - inputStream.close() - } - - zos.closeEntry() - zos.close() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to zip folder", e) - return false - } - return true - } - - /** - * Unzip the contents of a zip file to a folder. - * - * @param zipFile The zip file to unzip. - * @param destDirectory The destination directory. - * @return True if the operation is successful, false otherwise. - * @throws IOException If an I/O error occurs. - */ - @Throws(IOException::class) - fun unzipToFolder(zipFile: File, destDirectory: String): Boolean { - File(destDirectory).run { - if (!exists()) { - mkdirs() - } - } - try { - ZipFile(zipFile).use { zip -> - zip.entries().asSequence().forEach { entry -> - zip.getInputStream(entry).use { input -> - val filePath = destDirectory + File.separator + entry.name - if (!entry.isDirectory) { - extractFile(input, filePath) - } else { - val dir = File(filePath) - dir.mkdir() - } - } - } - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to unzip file", e) - return false - } - return true - } - - /** - * Extract a file from an input stream. - * - * @param inputStream The input stream to read from. - * @param destFilePath The destination file path. - * @throws IOException If an I/O error occurs. - */ - @Throws(IOException::class) - private fun extractFile(inputStream: InputStream, destFilePath: String) { - val bos = BufferedOutputStream(FileOutputStream(destFilePath)) - val bytesIn = ByteArray(BUFFER_SIZE) - var read: Int - while (inputStream.read(bytesIn).also { read = it } != -1) { - bos.write(bytesIn, 0, read) - } - bos.close() - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt deleted file mode 100644 index ec5cb7ee..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt +++ /dev/null @@ -1,447 +0,0 @@ -package com.v2ray.ang.viewmodel - -import android.app.Application -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.content.res.AssetManager -import android.util.Log -import androidx.core.content.ContextCompat -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import com.v2ray.ang.AngApplication -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.dto.ProfileItem -import com.v2ray.ang.dto.ServersCache -import com.v2ray.ang.extension.serializable -import com.v2ray.ang.extension.toastError -import com.v2ray.ang.extension.toastSuccess -import com.v2ray.ang.handler.AngConfigManager -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.handler.SettingsManager -import com.v2ray.ang.handler.SpeedtestManager -import com.v2ray.ang.util.MessageUtil -import com.v2ray.ang.util.Utils -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.launch -import java.util.Collections - -class MainViewModel(application: Application) : AndroidViewModel(application) { - private var serverList = MmkvManager.decodeServerList() - var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty() - - //var keywordFilter: String = MmkvManager.MmkvManager.decodeSettingsString(AppConfig.CACHE_KEYWORD_FILTER, "")?:"" - var keywordFilter = "" - val serversCache = mutableListOf() - val isRunning by lazy { MutableLiveData() } - val updateListAction by lazy { MutableLiveData() } - 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 - val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY) - ContextCompat.registerReceiver(getApplication(), mMsgReceiver, mFilter, Utils.receiverFlags()) - MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "") - } - - /** - * Called when the ViewModel is cleared. - */ - override fun onCleared() { - getApplication().unregisterReceiver(mMsgReceiver) - tcpingTestScope.coroutineContext[Job]?.cancelChildren() - SpeedtestManager.closeAllTcpSockets() - Log.i(AppConfig.TAG, "Main ViewModel is cleared") - super.onCleared() - } - - /** - * Reloads the server list. - */ - fun reloadServerList() { - serverList = MmkvManager.decodeServerList() - updateCache() - updateListAction.value = -1 - } - - /** - * Removes a server by its GUID. - * @param guid The GUID of the server to remove. - */ - fun removeServer(guid: String) { - serverList.remove(guid) - MmkvManager.removeServer(guid) - val index = getPosition(guid) - if (index >= 0) { - serversCache.removeAt(index) - } - } - -// /** -// * Appends a custom configuration server. -// * @param server The server configuration to append. -// * @return True if the server was successfully appended, false otherwise. -// */ -// fun appendCustomConfigServer(server: String): Boolean { -// if (server.contains("inbounds") -// && server.contains("outbounds") -// && server.contains("routing") -// ) { -// try { -// val config = CustomFmt.parse(server) ?: return false -// config.subscriptionId = subscriptionId -// val key = MmkvManager.encodeServerConfig("", config) -// MmkvManager.encodeServerRaw(key, server) -// serverList.add(0, key) -//// 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() -// } -// } -// return false -// } - - /** - * Swaps the positions of two servers. - * @param fromPosition The initial position of the server. - * @param toPosition The target position of the server. - */ - fun swapServer(fromPosition: Int, toPosition: Int) { - 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.encodeServerList(serverList) - } - - /** - * Updates the cache of servers. - */ - @Synchronized - fun updateCache() { - serversCache.clear() - for (guid in serverList) { - 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 - } - - if (keywordFilter.isEmpty() || profile.remarks.lowercase().contains(keywordFilter.lowercase())) { - serversCache.add(ServersCache(guid, profile)) - } - } - } - - /** - * Updates the configuration via subscription for all servers. - * @return The number of updated configurations. - */ - fun updateConfigViaSubAll(): Int { - if (subscriptionId.isEmpty()) { - return AngConfigManager.updateConfigViaSubAll() - } else { - val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return 0 - return AngConfigManager.updateConfigViaSub(Pair(subscriptionId, subItem)) - } - } - - /** - * Exports all servers. - * @return The number of exported servers. - */ - fun exportAllServer(): Int { - val serverListCopy = - if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) { - serverList - } else { - serversCache.map { it.guid }.toList() - } - - val ret = AngConfigManager.shareNonCustomConfigsToClipboard( - getApplication(), - serverListCopy - ) - return ret - } - - /** - * Tests the TCP ping for all servers. - */ - fun testAllTcping() { - tcpingTestScope.coroutineContext[Job]?.cancelChildren() - SpeedtestManager.closeAllTcpSockets() - MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList()) - - val serversCopy = serversCache.toList() - 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 = SpeedtestManager.tcping(serverAddress, serverPort.toInt()) - launch(Dispatchers.Main) { - MmkvManager.encodeServerTestDelayMillis(item.guid, testResult) - updateListAction.value = getPosition(item.guid) - } - } - } - } - } - } - - /** - * Tests the real ping for all servers. - */ - fun testAllRealPing() { - MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "") - MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList()) - updateListAction.value = -1 - - val serversCopy = serversCache.toList() - viewModelScope.launch(Dispatchers.Default) { - for (item in serversCopy) { - MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, item.guid) - } - } - } - - /** - * Tests the real ping for the current server. - */ - fun testCurrentServerRealPing() { - MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "") - } - - /** - * Changes the subscription ID. - * @param id The new subscription ID. - */ - fun subscriptionIdChanged(id: String) { - if (subscriptionId != id) { - subscriptionId = id - MmkvManager.encodeSettings(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId) - reloadServerList() - } - } - - /** - * Gets the subscriptions. - * @param context The context. - * @return A pair of lists containing the subscription IDs and remarks. - */ - fun getSubscriptions(context: Context): Pair?, MutableList?> { - val subscriptions = MmkvManager.decodeSubscriptions() - if (subscriptionId.isNotEmpty() - && !subscriptions.map { it.first }.contains(subscriptionId) - ) { - subscriptionIdChanged("") - } - if (subscriptions.isEmpty()) { - return null to null - } - val listId = subscriptions.map { it.first }.toMutableList() - listId.add(0, "") - val listRemarks = subscriptions.map { it.second.remarks }.toMutableList() - listRemarks.add(0, context.getString(R.string.filter_config_all)) - - return listId to listRemarks - } - - /** - * Gets the position of a server by its GUID. - * @param guid The GUID of the server. - * @return The position of the server. - */ - fun getPosition(guid: String): Int { - serversCache.forEachIndexed { index, it -> - if (it.guid == guid) - return index - } - return -1 - } - - /** - * Removes duplicate servers. - * @return The number of removed servers. - */ - fun removeDuplicateServer(): Int { - val serversCacheCopy = mutableListOf>() - for (it in serversCache) { - val config = MmkvManager.decodeServerConfig(it.guid) ?: continue - serversCacheCopy.add(Pair(it.guid, config)) - } - - val deleteServer = mutableListOf() - serversCacheCopy.forEachIndexed { index, it -> - val outbound = it.second - serversCacheCopy.forEachIndexed { index2, it2 -> - if (index2 > index) { - val outbound2 = it2.second - if (outbound.equals(outbound2) && !deleteServer.contains(it2.first)) { - deleteServer.add(it2.first) - } - } - } - } - for (it in deleteServer) { - MmkvManager.removeServer(it) - } - - return deleteServer.count() - } - - /** - * Removes all servers. - * @return The number of removed servers. - */ - 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 - } - - /** - * Removes invalid servers. - * @return The number of removed servers. - */ - fun removeInvalidServer(): Int { - var count = 0 - if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) { - count += MmkvManager.removeInvalidServer("") - } else { - val serversCopy = serversCache.toList() - for (item in serversCopy) { - count += MmkvManager.removeInvalidServer(item.guid) - } - } - return count - } - - /** - * Sorts servers by their test results. - */ - fun 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) - } - - MmkvManager.encodeServerList(serverList) - } - - /** - * Initializes assets. - * @param assets The asset manager. - */ - fun initAssets(assets: AssetManager) { - viewModelScope.launch(Dispatchers.Default) { - SettingsManager.initAssets(getApplication(), assets) - } - } - - /** - * Filters the configuration by a keyword. - * @param keyword The keyword to filter by. - */ - fun filterConfig(keyword: String) { - if (keyword == keywordFilter) { - return - } - keywordFilter = keyword - MmkvManager.encodeSettings(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter) - reloadServerList() - } - - private val mMsgReceiver = object : BroadcastReceiver() { - override fun onReceive(ctx: Context?, intent: Intent?) { - when (intent?.getIntExtra("key", 0)) { - AppConfig.MSG_STATE_RUNNING -> { - isRunning.value = true - } - - AppConfig.MSG_STATE_NOT_RUNNING -> { - isRunning.value = false - } - - AppConfig.MSG_STATE_START_SUCCESS -> { - getApplication().toastSuccess(R.string.toast_services_success) - isRunning.value = true - } - - AppConfig.MSG_STATE_START_FAILURE -> { - getApplication().toastError(R.string.toast_services_failure) - isRunning.value = false - } - - AppConfig.MSG_STATE_STOP_SUCCESS -> { - isRunning.value = false - } - - AppConfig.MSG_MEASURE_DELAY_SUCCESS -> { - updateTestResultAction.value = intent.getStringExtra("content") - } - - AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> { - val resultPair = intent.serializable>("content") ?: return - MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second) - updateListAction.value = getPosition(resultPair.first) - } - } - } - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt deleted file mode 100644 index 7ac5d60f..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.v2ray.ang.viewmodel - -import android.app.Application -import android.content.SharedPreferences -import android.util.Log -import androidx.lifecycle.AndroidViewModel -import androidx.preference.PreferenceManager -import com.v2ray.ang.AppConfig -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.handler.SettingsManager - -class SettingsViewModel(application: Application) : AndroidViewModel(application), - SharedPreferences.OnSharedPreferenceChangeListener { - - /** - * Starts listening for preference changes. - */ - fun startListenPreferenceChange() { - PreferenceManager.getDefaultSharedPreferences(getApplication()) - .registerOnSharedPreferenceChangeListener(this) - } - - /** - * Called when the ViewModel is cleared. - */ - override fun onCleared() { - PreferenceManager.getDefaultSharedPreferences(getApplication()) - .unregisterOnSharedPreferenceChangeListener(this) - Log.i(AppConfig.TAG, "Settings ViewModel is cleared") - super.onCleared() - } - - /** - * Called when a shared preference is changed. - * @param sharedPreferences The shared preferences. - * @param key The key of the changed preference. - */ - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { - Log.i(AppConfig.TAG, "Observe settings changed: $key") - when (key) { - AppConfig.PREF_MODE, - AppConfig.PREF_VPN_DNS, - AppConfig.PREF_VPN_BYPASS_LAN, - AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, - 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_LOGLEVEL, - AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, - AppConfig.PREF_LANGUAGE, - AppConfig.PREF_UI_MODE_NIGHT, - AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, - AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, - AppConfig.PREF_FRAGMENT_PACKETS, - AppConfig.PREF_FRAGMENT_LENGTH, - AppConfig.PREF_FRAGMENT_INTERVAL, - AppConfig.PREF_MUX_XUDP_QUIC, - -> { - 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, - AppConfig.PREF_BYPASS_APPS, - AppConfig.PREF_CONFIRM_REMOVE, - AppConfig.PREF_START_SCAN_IMMEDIATE, - AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, - AppConfig.SUBSCRIPTION_AUTO_UPDATE, - AppConfig.PREF_FRAGMENT_ENABLED, - AppConfig.PREF_MUX_ENABLED, - -> { - MmkvManager.encodeSettings(key, sharedPreferences.getBoolean(key, false)) - } - - AppConfig.PREF_SNIFFING_ENABLED -> { - MmkvManager.encodeSettings(key, sharedPreferences.getBoolean(key, true)) - } - - AppConfig.PREF_MUX_CONCURRENCY, - AppConfig.PREF_MUX_XUDP_CONCURRENCY -> { - MmkvManager.encodeSettings(key, sharedPreferences.getString(key, "8")) - } - } - if (key == AppConfig.PREF_UI_MODE_NIGHT) { - SettingsManager.setNightMode() - } - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt new file mode 100644 index 00000000..aa1ef7d4 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt @@ -0,0 +1,28 @@ +package com.v2ray.ang + +import androidx.multidex.MultiDexApplication +import androidx.preference.PreferenceManager +import com.tencent.mmkv.MMKV + +class AngApplication : MultiDexApplication() { + companion object { + const val PREF_LAST_VERSION = "pref_last_version" + } + + var firstRun = false + private set + + override fun onCreate() { + super.onCreate() + +// LeakCanary.install(this) + + val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) + firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE + 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) + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt new file mode 100644 index 00000000..3793e583 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt @@ -0,0 +1,89 @@ +package com.v2ray.ang + +/** + * + * App Config Const + */ +object AppConfig { + const val ANG_PACKAGE = "com.v2ray.ang" + const val DIR_ASSETS = "assets" + + // legacy + const val ANG_CONFIG = "ang_config" + const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium" + const val PREF_ROUTING_CUSTOM = "pref_routing_custom" + + // Preferences mapped to MMKV + const val PREF_MODE = "pref_mode" + const val PREF_SPEED_ENABLED = "pref_speed_enabled" + const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled" + const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled" + const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled" + const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled" + const val PREF_VPN_DNS = "pref_vpn_dns" + const val PREF_REMOTE_DNS = "pref_remote_dns" + const val PREF_DOMESTIC_DNS = "pref_domestic_dns" + const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port" + 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_LOGLEVEL = "pref_core_loglevel" + const val PREF_LANGUAGE = "pref_language" + const val PREF_PREFER_IPV6 = "pref_prefer_ipv6" + 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_PER_APP_PROXY = "pref_per_app_proxy" + const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set" + const val PREF_BYPASS_APPS = "pref_bypass_apps" + const val PREF_CONFIRM_REMOVE = "pref_confirm_remove" + + const val HTTP_PROTOCOL: String = "http://" + const val HTTPS_PROTOCOL: String = "https://" + + const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service" + const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity" + const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click" + + const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE" + const val TASKER_EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB" + const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch" + const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid" + const val TASKER_DEFAULT_GUID = "Default" + + const val TAG_AGENT = "proxy" + const val TAG_DIRECT = "direct" + const val TAG_BLOCKED = "block" + + const val androidpackagenamelistUrl = "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt" + const val v2rayCustomRoutingListUrl = "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/" + const val v2rayNGIssues = "https://github.com/2dust/v2rayNG/issues" + const val v2rayNGWikiMode = "https://github.com/2dust/v2rayNG/wiki/Mode" + const val promotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw=" + const val geoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/"; + + const val DNS_AGENT = "1.1.1.1" + const val DNS_DIRECT = "223.5.5.5" + + const val PORT_LOCAL_DNS = "10853" + const val PORT_SOCKS = "10808" + const val PORT_HTTP = "10809" + + const val MSG_REGISTER_CLIENT = 1 + const val MSG_STATE_RUNNING = 11 + const val MSG_STATE_NOT_RUNNING = 12 + const val MSG_UNREGISTER_CLIENT = 2 + const val MSG_STATE_START = 3 + const val MSG_STATE_START_SUCCESS = 31 + const val MSG_STATE_START_FAILURE = 32 + const val MSG_STATE_STOP = 4 + const val MSG_STATE_STOP_SUCCESS = 41 + const val MSG_STATE_RESTART = 5 + const val MSG_MEASURE_DELAY = 6 + const val MSG_MEASURE_DELAY_SUCCESS = 61 + const val MSG_MEASURE_CONFIG = 7 + const val MSG_MEASURE_CONFIG_SUCCESS = 71 + const val MSG_MEASURE_CONFIG_CANCEL = 72 +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt new file mode 100644 index 00000000..01f2bdd8 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt @@ -0,0 +1,32 @@ +package com.v2ray.ang.dto + +data class AngConfig( + var index: Int, + var vmess: ArrayList, + var subItem: ArrayList +) { + data class VmessBean(var guid: String = "123456", + var address: String = "v2ray.cool", + var port: Int = 10086, + var id: String = "a3482e88-686a-4a58-8126-99c9df64b7bf", + var alterId: Int = 64, + var security: String = "aes-128-cfb", + var network: String = "tcp", + var remarks: String = "def", + var headerType: String = "", + var requestHost: String = "", + var path: String = "", + var streamSecurity: String = "", + var allowInsecure: String = "", + var configType: Int = 1, + var configVersion: Int = 1, + var testResult: String = "", + var subid: String = "", + var flow: String = "", + var sni: String = "") + + data class SubItemBean(var id: String = "", + var remarks: String = "", + var url: String = "", + var enabled: Boolean = true) +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt new file mode 100644 index 00000000..f99655a8 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt @@ -0,0 +1,9 @@ +package com.v2ray.ang.dto + +import android.graphics.drawable.Drawable + +data class AppInfo(val appName: String, + val packageName: String, + val appIcon: Drawable, + val isSystemApp: Boolean, + var isSelected: Int) \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt new file mode 100644 index 00000000..5204c0ca --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt @@ -0,0 +1,15 @@ +package com.v2ray.ang.dto + +enum class EConfigType(val value: Int, val protocolScheme: String) { + VMESS(1, "vmess://"), + CUSTOM(2, ""), + SHADOWSOCKS(3, "ss://"), + SOCKS(4, "socks://"), + VLESS(5, "vless://"), + TROJAN(6, "trojan://"), + WIREGUARD(7, "wireguard://"); + + companion object { + fun fromInt(value: Int) = values().firstOrNull { it.value == value } + } +} 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 new file mode 100644 index 00000000..96d77397 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ERoutingMode.kt @@ -0,0 +1,9 @@ +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/java/com/v2ray/ang/dto/ServerAffiliationInfo.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerAffiliationInfo.kt similarity index 100% rename from V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerAffiliationInfo.kt rename to V2rayNG/app/src/main/kotlin/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/kotlin/com/v2ray/ang/dto/ServerConfig.kt new file mode 100644 index 00000000..88a9a7ec --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerConfig.kt @@ -0,0 +1,69 @@ +package com.v2ray.ang.dto + +import com.v2ray.ang.AppConfig.TAG_AGENT +import com.v2ray.ang.AppConfig.TAG_BLOCKED +import com.v2ray.ang.AppConfig.TAG_DIRECT +import com.v2ray.ang.util.Utils + +data class ServerConfig( + val configVersion: Int = 3, + val configType: EConfigType, + var subscriptionId: String = "", + val addedTime: Long = System.currentTimeMillis(), + var remarks: String = "", + val outboundBean: V2rayConfig.OutboundBean? = null, + var fullConfig: V2rayConfig? = null +) { + companion object { + fun create(configType: EConfigType): ServerConfig { + when(configType) { + EConfigType.VMESS, EConfigType.VLESS -> + return ServerConfig( + configType = configType, + outboundBean = V2rayConfig.OutboundBean( + protocol = configType.name.lowercase(), + settings = V2rayConfig.OutboundBean.OutSettingsBean( + vnext = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean( + users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())))), + streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean())) + EConfigType.CUSTOM, EConfigType.WIREGUARD -> + return ServerConfig(configType = configType) + EConfigType.SHADOWSOCKS, EConfigType.SOCKS, EConfigType.TROJAN -> + return ServerConfig( + configType = configType, + outboundBean = V2rayConfig.OutboundBean( + protocol = configType.name.lowercase(), + settings = V2rayConfig.OutboundBean.OutSettingsBean( + servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())), + streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean())) + } + } + } + + fun getProxyOutbound(): V2rayConfig.OutboundBean? { + if (configType != EConfigType.CUSTOM) { + return outboundBean + } + return fullConfig?.getProxyOutbound() + } + + fun getAllOutboundTags(): MutableList { + if (configType != EConfigType.CUSTOM) { + return mutableListOf(TAG_AGENT, TAG_DIRECT, TAG_BLOCKED) + } + fullConfig?.let { config -> + return config.outbounds.map { it.tag }.toMutableList() + } + return mutableListOf() + } + + fun getV2rayPointDomainAndPort(): String { + val address = getProxyOutbound()?.getServerAddress().orEmpty() + val port = getProxyOutbound()?.getServerPort() + return if (Utils.isIpv6Address(address)) { + String.format("[%s]:%s", address, port) + } else { + String.format("%s:%s", address, port) + } + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServersCache.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServersCache.kt new file mode 100644 index 00000000..e686b469 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServersCache.kt @@ -0,0 +1,4 @@ +package com.v2ray.ang.dto + +data class ServersCache(val guid: String, + val config: ServerConfig) \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt new file mode 100644 index 00000000..b2195148 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt @@ -0,0 +1,8 @@ +package com.v2ray.ang.dto + +data class SubscriptionItem( + var remarks: String = "", + var url: String = "", + var enabled: Boolean = true, + val addedTime: Long = System.currentTimeMillis()) { +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt new file mode 100644 index 00000000..cfc771b1 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt @@ -0,0 +1,462 @@ +package com.v2ray.ang.dto + +import android.text.TextUtils +import com.google.gson.GsonBuilder +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import com.google.gson.annotations.SerializedName +import com.google.gson.reflect.TypeToken +import java.lang.reflect.Type + +data class V2rayConfig( + var stats: Any? = null, + val log: LogBean, + var policy: PolicyBean?, + val inbounds: ArrayList, + var outbounds: ArrayList, + var dns: DnsBean, + val routing: RoutingBean, + val api: Any? = null, + val transport: Any? = null, + val reverse: Any? = null, + var fakedns: Any? = null, + val browserForwarder: 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 XTLS = "xtls" + const val HTTP = "http" + } + + data class LogBean(val access: String, + val error: String, + var loglevel: String?, + val dnsLog: Boolean? = null) + + data class InboundBean( + var tag: String, + var port: Int, + var protocol: String, + var listen: String? = null, + val settings: Any? = null, + val sniffing: SniffingBean?, + val streamSettings: Any? = null, + val allocate: Any? = null) { + + data class InSettingsBean(val auth: String? = null, + val udp: Boolean? = null, + val userLevel: Int? = null, + val address: String? = null, + val port: Int? = null, + val network: String? = null) + + data class SniffingBean(var enabled: Boolean, + val destOverride: ArrayList, + val metadataOnly: Boolean? = null) + } + + data class OutboundBean(val tag: String = "proxy", + var protocol: String, + var settings: OutSettingsBean? = null, + var streamSettings: StreamSettingsBean? = null, + val proxySettings: Any? = null, + val sendThrough: String? = null, + val mux: MuxBean? = MuxBean(false)) { + + data class OutSettingsBean(var vnext: List? = null, + var servers: List? = null, + /*Blackhole*/ + var response: Response? = null, + /*DNS*/ + val network: String? = null, + val 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*/ + val secretKey: String? = null, + val peers: List? = null, + ) { + + data class VnextBean(var address: String = "", + var port: Int = 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 = "") + } + + data class ServersBean(var address: String = "", + var method: String = "chacha20-poly1305", + var ota: Boolean = false, + var password: String = "", + var port: Int = DEFAULT_PORT, + var level: Int = 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) + } + + data class Response(var type: String) + + data class WireGuardBean(var publicKey: String = "", + var endpoint: String = "") + } + + data class StreamSettingsBean(var network: String = DEFAULT_NETWORK, + var security: String = "", + var tcpSettings: TcpSettingsBean? = null, + var kcpSettings: KcpSettingsBean? = null, + var wsSettings: WsSettingsBean? = null, + var httpSettings: HttpSettingsBean? = null, + var tlsSettings: TlsSettingsBean? = null, + var quicSettings: QuicSettingBean? = null, + var xtlsSettings: TlsSettingsBean? = null, + var grpcSettings: GrpcSettingsBean? = null, + val dsSettings: Any? = null, + val sockopt: Any? = null + ) { + + data class TcpSettingsBean(var header: HeaderBean = HeaderBean(), + val acceptProxyProtocol: Boolean? = null) { + data class HeaderBean(var type: String = "none", + var request: RequestBean? = null, + var response: Any? = null) { + data class RequestBean(var path: List = ArrayList(), + var headers: HeadersBean = HeadersBean(), + val version: String? = null, + val method: String? = null) { + data class HeadersBean(var Host: List = ArrayList(), + @SerializedName("User-Agent") + val userAgent: List? = null, + @SerializedName("Accept-Encoding") + val acceptEncoding: List? = null, + val Connection: List? = null, + val Pragma: String? = null) + } + } + } + + data class KcpSettingsBean(var mtu: Int = 1350, + var tti: Int = 50, + var uplinkCapacity: Int = 12, + var downlinkCapacity: Int = 100, + var congestion: Boolean = false, + var readBufferSize: Int = 1, + var writeBufferSize: Int = 1, + var header: HeaderBean = HeaderBean(), + var seed: String? = null) { + data class HeaderBean(var type: String = "none") + } + + data class WsSettingsBean(var path: String = "", + var headers: HeadersBean = HeadersBean(), + val maxEarlyData: Int? = null, + val useBrowserForwarding: Boolean? = null, + val acceptProxyProtocol: Boolean? = null) { + data class HeadersBean(var Host: String = "") + } + + data class HttpSettingsBean(var host: List = ArrayList(), + var path: String = "") + + data class TlsSettingsBean(var allowInsecure: Boolean = false, + var serverName: String = "", + val alpn: List? = null, + val minVersion: String? = null, + val maxVersion: String? = null, + val preferServerCipherSuites: Boolean? = null, + val cipherSuites: String? = null, + val fingerprint: String? = null, + val certificates: List? = null, + val disableSystemRoot: Boolean? = null, + val enableSessionResumption: Boolean? = null) + + data class QuicSettingBean(var security: String = "none", + var key: String = "", + var header: HeaderBean = HeaderBean()) { + data class HeaderBean(var type: String = "none") + } + + data class GrpcSettingsBean(var serviceName: String = "", + var multiMode: Boolean? = null) + + fun populateTransportSettings(transport: String, headerType: String?, host: String?, path: String?, seed: String?, + quicSecurity: String?, key: String?, mode: String?, serviceName: String?): String { + var sni = "" + network = transport + when (network) { + "tcp" -> { + val tcpSetting = TcpSettingsBean() + if (headerType == HTTP) { + tcpSetting.header.type = HTTP + if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) { + val requestObj = TcpSettingsBean.HeaderBean.RequestBean() + requestObj.headers.Host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() } + requestObj.path = (path ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() } + tcpSetting.header.request = requestObj + sni = requestObj.headers.Host.getOrNull(0) ?: sni + } + } else { + tcpSetting.header.type = "none" + sni = host ?: "" + } + tcpSettings = tcpSetting + } + "kcp" -> { + val kcpsetting = KcpSettingsBean() + kcpsetting.header.type = headerType ?: "none" + if (seed.isNullOrEmpty()) { + kcpsetting.seed = null + } else { + kcpsetting.seed = seed + } + kcpSettings = kcpsetting + } + "ws" -> { + val wssetting = WsSettingsBean() + wssetting.headers.Host = host ?: "" + sni = wssetting.headers.Host + wssetting.path = path ?: "/" + wsSettings = wssetting + } + "h2", "http" -> { + network = "h2" + val h2Setting = HttpSettingsBean() + h2Setting.host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() } + sni = h2Setting.host.getOrNull(0) ?: sni + h2Setting.path = path ?: "/" + httpSettings = h2Setting + } + "quic" -> { + val quicsetting = QuicSettingBean() + quicsetting.security = quicSecurity ?: "none" + quicsetting.key = key ?: "" + quicsetting.header.type = headerType ?: "none" + quicSettings = quicsetting + } + "grpc" -> { + val grpcSetting = GrpcSettingsBean() + grpcSetting.multiMode = mode == "multi" + grpcSetting.serviceName = serviceName ?: "" + sni = host ?: "" + grpcSettings = grpcSetting + } + } + return sni + } + + fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?) { + security = streamSecurity + val tlsSetting = TlsSettingsBean( + allowInsecure = allowInsecure, + serverName = sni, + fingerprint = fingerprint, + alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() } + ) + if (security == TLS) { + tlsSettings = tlsSetting + xtlsSettings = null + } else if (security == XTLS) { + tlsSettings = null + xtlsSettings = tlsSetting + } + } + } + + data class MuxBean(var enabled: Boolean, var concurrency: Int = 8) + + fun getServerAddress(): String? { + if (protocol.equals(EConfigType.VMESS.name, true) + || protocol.equals(EConfigType.VLESS.name, true)) { + return settings?.vnext?.get(0)?.address + } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) + || protocol.equals(EConfigType.SOCKS.name, true) + || protocol.equals(EConfigType.TROJAN.name, true)) { + return settings?.servers?.get(0)?.address + } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) { + return settings?.peers?.get(0)?.endpoint?.substringBeforeLast(":") + } + return null + } + + fun getServerPort(): Int? { + if (protocol.equals(EConfigType.VMESS.name, true) + || protocol.equals(EConfigType.VLESS.name, true)) { + return settings?.vnext?.get(0)?.port + } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) + || protocol.equals(EConfigType.SOCKS.name, true) + || protocol.equals(EConfigType.TROJAN.name, true)) { + return settings?.servers?.get(0)?.port + } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) { + return settings?.peers?.get(0)?.endpoint?.substringAfterLast(":")?.toInt() + } + return null + } + + 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 + } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) + || protocol.equals(EConfigType.TROJAN.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 + } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) { + return settings?.secretKey + } + return null + } + + fun getSecurityEncryption(): String? { + return when { + protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.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 + else -> null + } + } + + fun getTransportSettingDetails(): List? { + if (protocol.equals(EConfigType.VMESS.name, true) + || protocol.equals(EConfigType.VLESS.name, true) + || protocol.equals(EConfigType.TROJAN.name, true)) { + val transport = streamSettings?.network ?: return null + return when (transport) { + "tcp" -> { + val tcpSetting = streamSettings?.tcpSettings ?: return null + listOf(tcpSetting.header.type, + tcpSetting.header.request?.headers?.Host?.joinToString().orEmpty(), + tcpSetting.header.request?.path?.joinToString().orEmpty()) + } + "kcp" -> { + val kcpSetting = streamSettings?.kcpSettings ?: return null + listOf(kcpSetting.header.type, + "", + kcpSetting.seed.orEmpty()) + } + "ws" -> { + val wsSetting = streamSettings?.wsSettings ?: return null + listOf("", + wsSetting.headers.Host, + wsSetting.path) + } + "h2" -> { + val h2Setting = streamSettings?.httpSettings ?: return null + listOf("", + h2Setting.host.joinToString(), + h2Setting.path) + } + "quic" -> { + val quicSetting = streamSettings?.quicSettings ?: return null + listOf(quicSetting.header.type, + quicSetting.security, + quicSetting.key) + } + "grpc" -> { + val grpcSetting = streamSettings?.grpcSettings ?: return null + listOf(if (grpcSetting.multiMode == true) "multi" else "gun", + "", + grpcSetting.serviceName) + } + else -> null + } + } + return null + } + } + + data class DnsBean(var servers: ArrayList? = null, + var hosts: Map? = null, + val clientIp: String? = null, + val disableCache: Boolean? = null, + val queryStrategy: String? = null, + val tag: String? = null + ) { + data class ServersBean(var address: String = "", + var port: Int? = null, + var domains: List? = null, + var expectIPs: List? = null, + val clientIp: String? = null) + } + + data class RoutingBean(var domainStrategy: String, + var domainMatcher: String? = null, + var rules: ArrayList, + val balancers: List? = null) { + + data class RulesBean(var type: String = "", + var ip: ArrayList? = null, + var domain: ArrayList? = null, + var outboundTag: String = "", + var balancerTag: String? = null, + var port: String? = null, + val sourcePort: String? = null, + val network: String? = null, + val source: List? = null, + val user: List? = null, + var inboundTag: List? = null, + val protocol: List? = null, + val attrs: String? = null, + val domainMatcher: String? = null + ) + } + + data class PolicyBean(var levels: Map, + var system: Any? = null) { + data class LevelBean( + var handshake: Int? = null, + var connIdle: Int? = null, + var uplinkOnly: Int? = null, + var downlinkOnly: Int? = null, + val statsUserUplink: Boolean? = null, + val statsUserDownlink: Boolean? = null, + var bufferSize: Int? = null) + } + + data class FakednsBean(var ipPool: String = "198.18.0.0/15", + var poolSize: Int = 10000) // roughly 10 times smaller than total ip pool + + fun getProxyOutbound(): OutboundBean? { + outbounds.forEach { outbound -> + EConfigType.values().forEach { + if (outbound.protocol.equals(it.name, true)) { + return outbound + } + } + } + return null + } + + fun toPrettyPrinting(): String { + return 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() + .toJson(this) + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt new file mode 100644 index 00000000..48f3a341 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt @@ -0,0 +1,16 @@ +package com.v2ray.ang.dto + +data class VmessQRCode(var v: String = "", + var ps: String = "", + var add: String = "", + var port: String = "", + var id: String = "", + var aid: String = "0", + var scy: String = "", + var net: String = "", + var type: String = "", + var host: String = "", + var path: String = "", + var tls: String = "", + var sni: String = "", + var alpn: String = "") \ No newline at end of file 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 new file mode 100644 index 00000000..fb0bff63 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt @@ -0,0 +1,80 @@ +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 + +/** + * Some extensions + */ + +val Context.v2RayApplication: AngApplication + get() = applicationContext as AngApplication + +fun Context.toast(message: Int): Toast = ToastCompat + .makeText(this, message, Toast.LENGTH_SHORT) + .apply { + show() + } + +fun Context.toast(message: CharSequence): Toast = ToastCompat + .makeText(this, message, Toast.LENGTH_SHORT) + .apply { + show() + } + +fun JSONObject.putOpt(pair: Pair) = putOpt(pair.first, pair.second) +fun JSONObject.putOpt(pairs: Map) = pairs.forEach { putOpt(it.key to it.value) } + +const val threshold = 1000 +const val divisor = 1024F + +fun Long.toSpeedString() = toTrafficString() + "/s" + +fun Long.toTrafficString(): String { + if (this == 0L) + return "\t\t\t0\t B" + + if (this < threshold) + return "${this.toFloat().toShortString()}\t B" + + val kib = this / divisor + if (kib < threshold) + return "${kib.toShortString()}\t KB" + + val mib = kib / divisor + if (mib < threshold) + return "${mib.toShortString()}\t MB" + + val gib = mib / divisor + if (gib < threshold) + return "${gib.toShortString()}\t GB" + + val tib = gib / divisor + if (tib < threshold) + return "${tib.toShortString()}\t TB" + + val pib = tib / divisor + if (pib < threshold) + return "${pib.toShortString()}\t PB" + + return "∞" +} + +private fun Float.toShortString(): String { + val s = "%.2f".format(this) + if (s.length <= 4) + return s + return s.substring(0, 4).removeSuffix(".") +} + +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("]", "") \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/TaskerReceiver.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt similarity index 50% rename from V2rayNG/app/src/main/java/com/v2ray/ang/receiver/TaskerReceiver.kt rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt index bdb7bf9a..08a3184a 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/TaskerReceiver.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt @@ -4,38 +4,38 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.text.TextUtils +import com.google.zxing.WriterException +import com.tencent.mmkv.MMKV import com.v2ray.ang.AppConfig 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) } - /** - * This method is called when the BroadcastReceiver is receiving an Intent broadcast. - * It retrieves the bundle from the intent and checks the switch and guid values. - * Depending on the switch value, it starts or stops the V2Ray service. - * - * @param context The Context in which the receiver is running. - * @param intent The Intent being received. - */ 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).orEmpty() + val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "") - if (switch == null || TextUtils.isEmpty(guid)) { + if (switch == null || guid == null || TextUtils.isEmpty(guid)) { return } else if (switch) { if (guid == AppConfig.TASKER_DEFAULT_GUID) { - V2RayServiceManager.startVServiceFromToggle(context) + Utils.startVServiceFromToggle(context) } else { - V2RayServiceManager.startVService(context, guid) + mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid) + V2RayServiceManager.startV2Ray(context) } } else { - V2RayServiceManager.stopVService(context) + Utils.stopVService(context) } - } catch (e: Exception) { - android.util.Log.e(AppConfig.TAG, "Error processing Tasker broadcast", e) + } catch (e: WriterException) { + e.printStackTrace() } } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/WidgetProvider.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt similarity index 52% rename from V2rayNG/app/src/main/java/com/v2ray/ang/receiver/WidgetProvider.kt rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt index 99d9bda2..826bf24f 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/WidgetProvider.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt @@ -8,32 +8,21 @@ import android.content.Context import android.content.Intent import android.os.Build import android.widget.RemoteViews -import com.v2ray.ang.AppConfig import com.v2ray.ang.R +import com.v2ray.ang.AppConfig import com.v2ray.ang.service.V2RayServiceManager +import com.v2ray.ang.util.Utils class WidgetProvider : AppWidgetProvider() { /** - * This method is called every time the widget is updated. - * It updates the widget background based on the V2Ray service running state. - * - * @param context The Context in which the receiver is running. - * @param appWidgetManager The AppWidgetManager instance. - * @param appWidgetIds The appWidgetIds for which an update is needed. + * 每次窗口小部件被更新都调用一次该方法 */ override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { super.onUpdate(context, appWidgetManager, appWidgetIds) - updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.isRunning()) + updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.v2rayPoint.isRunning) } - /** - * Updates the widget background based on whether the V2Ray service is running. - * - * @param context The Context in which the receiver is running. - * @param appWidgetManager The AppWidgetManager instance. - * @param appWidgetIds The appWidgetIds for which an update is needed. - * @param isRunning Boolean indicating if the V2Ray service is running. - */ + private fun updateWidgetBackground(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, isRunning: Boolean) { val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch) val intent = Intent(context, WidgetProvider::class.java) @@ -46,15 +35,20 @@ class WidgetProvider : AppWidgetProvider() { PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT } else { PendingIntent.FLAG_UPDATE_CURRENT - } - ) + }) remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent) if (isRunning) { - remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_stop_24dp) - remoteViews.setInt(R.id.layout_background, "setBackgroundResource", R.drawable.ic_rounded_corner_active) + remoteViews.setInt( + R.id.layout_switch, + "setBackgroundResource", + R.drawable.ic_rounded_corner_theme + ) } else { - remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_play_24dp) - remoteViews.setInt(R.id.layout_background, "setBackgroundResource", R.drawable.ic_rounded_corner_inactive) + remoteViews.setInt( + R.id.layout_switch, + "setBackgroundResource", + R.drawable.ic_rounded_corner_grey + ) } for (appWidgetId in appWidgetIds) { @@ -63,35 +57,26 @@ class WidgetProvider : AppWidgetProvider() { } /** - * This method is called when the BroadcastReceiver is receiving an Intent broadcast. - * It handles widget click actions and updates the widget background based on the V2Ray service state. - * - * @param context The Context in which the receiver is running. - * @param intent The Intent being received. + * 接收窗口小部件发送的广播 */ override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent) if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) { - if (V2RayServiceManager.isRunning()) { - V2RayServiceManager.stopVService(context) + if (V2RayServiceManager.v2rayPoint.isRunning) { + Utils.stopVService(context) } else { - V2RayServiceManager.startVServiceFromToggle(context) + Utils.startVServiceFromToggle(context) } } else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) { AppWidgetManager.getInstance(context)?.let { manager -> when (intent.getIntExtra("key", 0)) { AppConfig.MSG_STATE_RUNNING, AppConfig.MSG_STATE_START_SUCCESS -> { - updateWidgetBackground( - context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)), - true - ) + updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)), + true) } - AppConfig.MSG_STATE_NOT_RUNNING, AppConfig.MSG_STATE_START_FAILURE, AppConfig.MSG_STATE_STOP_SUCCESS -> { - updateWidgetBackground( - context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)), - false - ) + updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)), + false) } } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt similarity index 58% rename from V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt index 7aecf634..176b0fbe 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt @@ -1,5 +1,6 @@ package com.v2ray.ang.service +import android.annotation.TargetApi import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -8,80 +9,52 @@ import android.graphics.drawable.Icon import android.os.Build import android.service.quicksettings.Tile import android.service.quicksettings.TileService -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.core.content.ContextCompat import com.v2ray.ang.AppConfig import com.v2ray.ang.R import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.Utils import java.lang.ref.SoftReference -@RequiresApi(Build.VERSION_CODES.N) +@TargetApi(Build.VERSION_CODES.N) class QSTileService : TileService() { - /** - * Sets the state of the tile. - * @param state The state to set. - */ fun setState(state: Int) { - qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name) if (state == Tile.STATE_INACTIVE) { qsTile?.state = Tile.STATE_INACTIVE qsTile?.label = getString(R.string.app_name) + qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name) } else if (state == Tile.STATE_ACTIVE) { qsTile?.state = Tile.STATE_ACTIVE - qsTile?.label = V2RayServiceManager.getRunningServerName() + qsTile?.label = V2RayServiceManager.currentConfig?.remarks + qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name) } 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() - - if (V2RayServiceManager.isRunning()) { - setState(Tile.STATE_ACTIVE) - } else { - setState(Tile.STATE_INACTIVE) - } + setState(Tile.STATE_INACTIVE) mMsgReceive = ReceiveMessageHandler(this) - val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY) - ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags()) + registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)) MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "") } - /** - * Called when the tile stops listening. - */ override fun onStopListening() { super.onStopListening() - try { - applicationContext.unregisterReceiver(mMsgReceive) - mMsgReceive = null - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to unregister receiver", e) - } - + unregisterReceiver(mMsgReceive) + mMsgReceive = null } - /** - * Called when the tile is clicked. - */ override fun onClick() { super.onClick() when (qsTile.state) { Tile.STATE_INACTIVE -> { - V2RayServiceManager.startVServiceFromToggle(this) + Utils.startVServiceFromToggle(this) } - Tile.STATE_ACTIVE -> { - V2RayServiceManager.stopVService(this) + Utils.stopVService(this) } } } @@ -89,26 +62,22 @@ class QSTileService : TileService() { private var mMsgReceive: BroadcastReceiver? = null private class ReceiveMessageHandler(context: QSTileService) : BroadcastReceiver() { - var mReference: SoftReference = SoftReference(context) + internal var mReference: SoftReference = SoftReference(context) override fun onReceive(ctx: Context?, intent: Intent?) { val context = mReference.get() when (intent?.getIntExtra("key", 0)) { AppConfig.MSG_STATE_RUNNING -> { context?.setState(Tile.STATE_ACTIVE) } - AppConfig.MSG_STATE_NOT_RUNNING -> { context?.setState(Tile.STATE_INACTIVE) } - AppConfig.MSG_STATE_START_SUCCESS -> { context?.setState(Tile.STATE_ACTIVE) } - AppConfig.MSG_STATE_START_FAILURE -> { context?.setState(Tile.STATE_INACTIVE) } - AppConfig.MSG_STATE_STOP_SUCCESS -> { context?.setState(Tile.STATE_INACTIVE) } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/ServiceControl.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/ServiceControl.kt new file mode 100644 index 00000000..36a26b88 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/ServiceControl.kt @@ -0,0 +1,13 @@ +package com.v2ray.ang.service + +import android.app.Service + +interface ServiceControl { + fun getService(): Service + + fun startService() + + fun stopService() + + fun vpnProtect(socket: Int): Boolean +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayProxyOnlyService.kt similarity index 53% rename from V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayProxyOnlyService.kt index 25fcd1a6..3403af61 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayProxyOnlyService.kt @@ -6,87 +6,50 @@ import android.content.Intent import android.os.Build import android.os.IBinder import androidx.annotation.RequiresApi -import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.util.MyContextWrapper +import com.v2ray.ang.util.Utils import java.lang.ref.SoftReference class V2RayProxyOnlyService : Service(), ServiceControl { - /** - * Initializes the service. - */ override fun onCreate() { super.onCreate() V2RayServiceManager.serviceControl = SoftReference(this) } - /** - * Handles the start command for the service. - * @param intent The intent. - * @param flags The flags. - * @param startId The start ID. - * @return The start mode. - */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - V2RayServiceManager.startCoreLoop() + V2RayServiceManager.startV2rayPoint() return START_STICKY } - /** - * Destroys the service. - */ override fun onDestroy() { super.onDestroy() - V2RayServiceManager.stopCoreLoop() + V2RayServiceManager.stopV2rayPoint() } - /** - * Gets the service instance. - * @return The service instance. - */ override fun getService(): Service { return this } - /** - * Starts the service. - */ override fun startService() { // do nothing } - /** - * Stops the service. - */ override fun stopService() { stopSelf() } - /** - * Protects the VPN socket. - * @param socket The socket to protect. - * @return True if the socket is protected, false otherwise. - */ override fun vpnProtect(socket: Int): Boolean { return true } - /** - * Binds the service. - * @param intent The intent. - * @return The binder. - */ override fun onBind(intent: Intent?): IBinder? { return null } - /** - * Attaches the base context to the service. - * @param newBase The new base context. - */ @RequiresApi(Build.VERSION_CODES.N) override fun attachBaseContext(newBase: Context?) { val context = newBase?.let { - MyContextWrapper.wrap(newBase, SettingsManager.getLocale()) + MyContextWrapper.wrap(newBase, Utils.getLocale(newBase)) } super.attachBaseContext(context) } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt new file mode 100644 index 00000000..15f4a1e8 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt @@ -0,0 +1,399 @@ +package com.v2ray.ang.service + +import android.app.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Color +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.AppConfig.TAG_DIRECT +import com.v2ray.ang.R +import com.v2ray.ang.dto.ServerConfig +import com.v2ray.ang.extension.toSpeedString +import com.v2ray.ang.extension.toast +import com.v2ray.ang.ui.MainActivity +import com.v2ray.ang.util.MessageUtil +import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.util.Utils +import com.v2ray.ang.util.V2rayConfigUtil +import go.Seq +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import libv2ray.Libv2ray +import libv2ray.V2RayPoint +import libv2ray.V2RayVPNServiceSupportsSet +import rx.Observable +import rx.Subscription +import java.lang.ref.SoftReference +import kotlin.math.min + +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_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) { + field = value + Seq.setContext(value?.get()?.getService()?.applicationContext) + Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService())) + } + var currentConfig: ServerConfig? = null + + private var lastQueryTime = 0L + private var mBuilder: NotificationCompat.Builder? = null + private var mSubscription: Subscription? = null + private var mNotificationManager: NotificationManager? = null + + fun startV2Ray(context: Context) { + if (settingsStorage?.decodeBool(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") { + Intent(context.applicationContext, V2RayVpnService::class.java) + } else { + Intent(context.applicationContext, V2RayProxyOnlyService::class.java) + } + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + + private class V2RayCallback : V2RayVPNServiceSupportsSet { + override fun shutdown(): Long { + val serviceControl = serviceControl?.get() ?: return -1 + // called by go + return try { + serviceControl.stopService() + 0 + } catch (e: Exception) { + Log.d(ANG_PACKAGE, e.toString()) + -1 + } + } + + override fun prepare(): Long { + return 0 + } + + override fun protect(l: Long): Boolean { + val serviceControl = serviceControl?.get() ?: return true + return serviceControl.vpnProtect(l.toInt()) + } + + 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() + startSpeedNotification() + 0 + } catch (e: Exception) { + Log.d(ANG_PACKAGE, e.toString()) + -1 + } + } + } + + fun startV2rayPoint() { + val service = serviceControl?.get()?.getService() ?: return + val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return + val config = MmkvManager.decodeServerConfig(guid) ?: return + if (!v2rayPoint.isRunning) { + val result = V2rayConfigUtil.getV2rayConfig(service, guid) + if (!result.status) + return + + try { + val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE) + mFilter.addAction(Intent.ACTION_SCREEN_ON) + mFilter.addAction(Intent.ACTION_SCREEN_OFF) + mFilter.addAction(Intent.ACTION_USER_PRESENT) + service.registerReceiver(mMsgReceive, mFilter) + } catch (e: Exception) { + Log.d(ANG_PACKAGE, e.toString()) + } + + v2rayPoint.configureFileContent = result.content + v2rayPoint.domainName = config.getV2rayPointDomainAndPort() + currentConfig = config + + try { + v2rayPoint.runLoop(settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) ?: false) + } catch (e: Exception) { + Log.d(ANG_PACKAGE, e.toString()) + } + + if (v2rayPoint.isRunning) { + MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "") + showNotification() + } else { + MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "") + cancelNotification() + } + } + } + + fun stopV2rayPoint() { + val service = serviceControl?.get()?.getService() ?: return + + if (v2rayPoint.isRunning) { + GlobalScope.launch(Dispatchers.Default) { + try { + v2rayPoint.stopLoop() + } catch (e: Exception) { + Log.d(ANG_PACKAGE, e.toString()) + } + } + } + + MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "") + cancelNotification() + + try { + service.unregisterReceiver(mMsgReceive) + } catch (e: Exception) { + Log.d(ANG_PACKAGE, e.toString()) + } + } + + private class ReceiveMessageHandler : BroadcastReceiver() { + override fun onReceive(ctx: Context?, intent: Intent?) { + 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 { + MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "") + } + } + AppConfig.MSG_UNREGISTER_CLIENT -> { + // nothing to do + } + AppConfig.MSG_STATE_START -> { + // nothing to do + } + AppConfig.MSG_STATE_STOP -> { + serviceControl.stopService() + } + AppConfig.MSG_STATE_RESTART -> { + startV2rayPoint() + } + AppConfig.MSG_MEASURE_DELAY -> { + measureV2rayDelay() + } + } + + when (intent?.action) { + Intent.ACTION_SCREEN_OFF -> { + Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats") + stopSpeedNotification() + } + Intent.ACTION_SCREEN_ON -> { + Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats") + startSpeedNotification() + } + } + } + } + + private fun measureV2rayDelay() { + GlobalScope.launch(Dispatchers.IO) { + val service = serviceControl?.get()?.getService() ?: return@launch + var time = -1L + var errstr = "" + if (v2rayPoint.isRunning) { + try { + time = v2rayPoint.measureDelay() + } catch (e: Exception) { + Log.d(ANG_PACKAGE, "measureV2rayDelay: $e") + errstr = e.message?.substringAfter("\":") ?: "empty message" + } + } + val result = if (time == -1L) { + service.getString(R.string.connection_test_error, errstr) + } else { + service.getString(R.string.connection_test_available, time) + } + + MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result) + } + } + + private fun showNotification() { + val service = serviceControl?.get()?.getService() ?: return + 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 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, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + } else { + PendingIntent.FLAG_UPDATE_CURRENT + }) + + val channelId = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel() + } else { + // If earlier version channel ID is not used + // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) + "" + } + + mBuilder = NotificationCompat.Builder(service, channelId) + .setSmallIcon(R.drawable.ic_stat_name) + .setContentTitle(currentConfig?.remarks) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setOngoing(true) + .setShowWhen(false) + .setOnlyAlertOnce(true) + .setContentIntent(contentPendingIntent) + .addAction(R.drawable.ic_close_grey_800_24dp, + service.getString(R.string.notification_action_stop_v2ray), + stopV2RayPendingIntent) + //.build() + + //mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使 + + service.startForeground(NOTIFICATION_ID, mBuilder?.build()) + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel(): String { + val channelId = "RAY_NG_M_CH_ID" + val channelName = "V2rayNG Background Service" + val chan = NotificationChannel(channelId, + channelName, NotificationManager.IMPORTANCE_HIGH) + chan.lightColor = Color.DKGRAY + chan.importance = NotificationManager.IMPORTANCE_NONE + chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + getNotificationManager()?.createNotificationChannel(chan) + return channelId + } + + fun cancelNotification() { + val service = serviceControl?.get()?.getService() ?: return + service.stopForeground(true) + mBuilder = null + mSubscription?.unsubscribe() + mSubscription = null + } + + private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) { + if (mBuilder != null) { + if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) { + mBuilder?.setSmallIcon(R.drawable.ic_stat_name) + } else if (proxyTraffic > directTraffic) { + mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy) + } else { + mBuilder?.setSmallIcon(R.drawable.ic_stat_direct) + } + mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) + mBuilder?.setContentText(contentText) // Emui4.1 need content text even if style is set as BigTextStyle + getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build()) + } + } + + private fun getNotificationManager(): NotificationManager? { + if (mNotificationManager == null) { + val service = serviceControl?.get()?.getService() ?: return null + mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + } + return mNotificationManager + } + + private fun startSpeedNotification() { + if (mSubscription == null && + v2rayPoint.isRunning && + settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true) { + var lastZeroSpeed = false + val outboundTags = currentConfig?.getAllOutboundTags() + outboundTags?.remove(TAG_DIRECT) + + mSubscription = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS) + .subscribe { + val queryTime = System.currentTimeMillis() + val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0 + var proxyTotal = 0L + val text = StringBuilder() + outboundTags?.forEach { + val up = v2rayPoint.queryStats(it, "uplink") + val down = v2rayPoint.queryStats(it, "downlink") + if (up + down > 0) { + appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds) + proxyTotal += up + down + } + } + val directUplink = v2rayPoint.queryStats(TAG_DIRECT, "uplink") + val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, "downlink") + val zeroSpeed = (proxyTotal == 0L && directUplink == 0L && directDownlink == 0L) + if (!zeroSpeed || !lastZeroSpeed) { + if (proxyTotal == 0L) { + appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0) + } + appendSpeedString(text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds, + directDownlink / sinceLastQueryInSeconds) + updateNotification(text.toString(), proxyTotal, directDownlink + directUplink) + } + lastZeroSpeed = zeroSpeed + lastQueryTime = queryTime + } + } + } + + private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) { + var n = name ?: "no tag" + n = n.substring(0, min(n.length, 6)) + text.append(n) + for (i in n.length..6 step 2) { + text.append("\t") + } + text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n") + } + + private fun stopSpeedNotification() { + if (mSubscription != null) { + mSubscription?.unsubscribe() //stop queryStats + mSubscription = 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/kotlin/com/v2ray/ang/service/V2RayTestService.kt new file mode 100644 index 00000000..362b7196 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayTestService.kt @@ -0,0 +1,45 @@ +package com.v2ray.ang.service + +import android.app.Service +import android.content.Intent +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.util.MessageUtil +import com.v2ray.ang.util.SpeedtestUtil +import com.v2ray.ang.util.Utils +import go.Seq +import kotlinx.coroutines.* +import libv2ray.Libv2ray +import java.util.concurrent.Executors + +class V2RayTestService : Service() { + private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(10).asCoroutineDispatcher()) } + + override fun onCreate() { + super.onCreate() + Seq.setContext(this) + Libv2ray.initV2Env(Utils.userAssetPath(this)) + } + + 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 + realTestScope.launch { + val result = SpeedtestUtil.realPing(contentPair.second) + MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(contentPair.first, result)) + } + } + MSG_MEASURE_CONFIG_CANCEL -> { + realTestScope.coroutineContext[Job]?.cancelChildren() + } + } + return super.onStartCommand(intent, flags, startId) + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt similarity index 56% rename from V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt index d734c299..f328a9b9 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt @@ -4,28 +4,21 @@ import android.app.Service import android.content.Context import android.content.Intent import android.content.pm.PackageManager -import android.net.ConnectivityManager -import android.net.LocalSocket -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.net.* 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.LOOPBACK -import com.v2ray.ang.BuildConfig -import com.v2ray.ang.handler.MmkvManager -import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.R +import com.v2ray.ang.dto.ERoutingMode +import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MyContextWrapper import com.v2ray.ang.util.Utils -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.io.File import java.lang.ref.SoftReference @@ -33,11 +26,18 @@ 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 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 + + //val fd: Int get() = mInterface.fd private lateinit var process: Process /**destroy @@ -52,12 +52,12 @@ class V2RayVpnService : VpnService(), ServiceControl { @delegate:RequiresApi(Build.VERSION_CODES.P) private val defaultNetworkRequest by lazy { NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) - .build() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + .build() } - private val connectivity by lazy { getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager } + private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } @delegate:RequiresApi(Build.VERSION_CODES.P) private val defaultNetworkCallback by lazy { @@ -79,6 +79,7 @@ class V2RayVpnService : VpnService(), ServiceControl { override fun onCreate() { super.onCreate() + val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() StrictMode.setThreadPolicy(policy) V2RayServiceManager.serviceControl = SoftReference(this) @@ -95,17 +96,205 @@ class V2RayVpnService : VpnService(), ServiceControl { override fun onDestroy() { super.onDestroy() - NotificationService.cancelNotification() + V2RayServiceManager.cancelNotification() + } + + private fun setup() { + val prepare = prepare(this) + if (prepare != null) { + return + } + + // If the old interface has exactly the same parameters, use it! + // Configure a builder while parsing the parameters. + val builder = Builder() + //val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false) + + val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.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) { + resources.getStringArray(R.array.bypass_private_ip_address).forEach { + val addr = it.split('/') + builder.addRoute(addr[0], addr[1].toInt()) + } + } else { + builder.addRoute("0.0.0.0", 0) + } + + if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) { + builder.addAddress(PRIVATE_VLAN6_CLIENT, 126) + if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) { + 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) + } + } + } + + 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 + apps?.forEach { + try { + if (bypassApps) + builder.addDisallowedApplication(it) + else + builder.addAllowedApplication(it) + } catch (e: PackageManager.NameNotFoundException) { + //Logger.d(e) + } + } + } + + // Close the old interface since the parameters have been changed. + try { + mInterface.close() + } catch (ignored: Exception) { + // ignored + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + try { + connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback) + } catch (e: Exception) { + e.printStackTrace() + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + builder.setMetered(false) + } + + // Create a new interface using the builder and save the parameters. + try { + mInterface = builder.establish()!! + runTun2socks() + } catch (e: Exception) { + // non-nullable lateinit var + e.printStackTrace() + stopV2Ray() + } + } + + private fun runTun2socks() { + val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt()) + 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}", + "--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) { + 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()) + cmd.add("--dnsgw") + cmd.add("127.0.0.1:${localDnsPort}") + } + Log.d(packageName, cmd.toString()) + + try { + val proBuilder = ProcessBuilder(cmd) + proBuilder.redirectErrorStream(true) + process = proBuilder + .directory(applicationContext.filesDir) + .start() + Log.d(packageName, process.toString()) + + sendFd() + } catch (e: Exception) { + Log.d(packageName, e.toString()) + } + } + + private fun sendFd() { + val fd = mInterface.fileDescriptor + val path = File(applicationContext.filesDir, "sock_path").absolutePath + Log.d(packageName, path) + + GlobalScope.launch(Dispatchers.IO) { + var tries = 0 + while (true) try { + Thread.sleep(50L shl tries) + Log.d(packageName, "sendFd tries: $tries") + LocalSocket().use { localSocket -> + localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM)) + localSocket.setFileDescriptorsForSend(arrayOf(fd)) + localSocket.outputStream.write(42) + } + break + } catch (e: Exception) { + Log.d(packageName, e.toString()) + if (tries > 5) break + tries += 1 + } + } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (V2RayServiceManager.startCoreLoop()) { - startService() - } + V2RayServiceManager.startV2rayPoint() return START_STICKY //return super.onStartCommand(intent, flags, startId) } + private fun stopV2Ray(isForced: Boolean = true) { +// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "") +// val emptyInfo = VpnNetworkInfo() +// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo) +// saveVpnNetworkInfo(configName, info) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + try { + connectivity.unregisterNetworkCallback(defaultNetworkCallback) + } catch (ignored: Exception) { + // ignored + } + } + + try { + Log.d(packageName, "tun2socks destroy") + process.destroy() + } catch (e: Exception) { + Log.d(packageName, e.toString()) + } + + V2RayServiceManager.stopV2rayPoint() + + if (isForced) { + //stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped + //It's strage but true. + //This can be verified by putting stopself() behind and call stopLoop and startLoop + //in a row for several times. You will find that later created v2ray core report port in use + //which means the first v2ray core somehow failed to stop and release the port. + stopSelf() + + try { + mInterface.close() + } catch (ignored: Exception) { + // ignored + } + } + } + override fun getService(): Service { return this } @@ -125,251 +314,8 @@ class V2RayVpnService : VpnService(), ServiceControl { @RequiresApi(Build.VERSION_CODES.N) override fun attachBaseContext(newBase: Context?) { val context = newBase?.let { - MyContextWrapper.wrap(newBase, SettingsManager.getLocale()) + MyContextWrapper.wrap(newBase, Utils.getLocale(newBase)) } super.attachBaseContext(context) } - - /** - * Sets up the VPN service. - * Prepares the VPN and configures it if preparation is successful. - */ - private fun setup() { - val prepare = prepare(this) - if (prepare != null) { - return - } - - if (setupVpnService() != true) { - return - } - - runTun2socks() - } - - /** - * Configures the VPN service. - * @return True if the VPN service was configured successfully, false otherwise. - */ - private fun setupVpnService(): Boolean { - // If the old interface has exactly the same parameters, use it! - // Configure a builder while parsing the parameters. - val builder = Builder() - val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig() - //val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false) - - builder.setMtu(VPN_MTU) - builder.addAddress(vpnConfig.ipv4Client, 30) - //builder.addDnsServer(PRIVATE_VLAN4_ROUTER) - val bypassLan = SettingsManager.routingRulesetsBypassLan() - if (bypassLan) { - AppConfig.ROUTED_IP_LIST.forEach { - val addr = it.split('/') - builder.addRoute(addr[0], addr[1].toInt()) - } - } else { - builder.addRoute("0.0.0.0", 0) - } - - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) { - builder.addAddress(vpnConfig.ipv6Client, 126) - if (bypassLan) { - builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use - builder.addRoute("fc00::", 18) //Xray-core default FakeIPv6 Pool - } else { - builder.addRoute("::", 0) - } - } - -// if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { -// builder.addDnsServer(PRIVATE_VLAN4_ROUTER) -// } else { - SettingsManager.getVpnDnsServers() - .forEach { - if (Utils.isPureIpAddress(it)) { - builder.addDnsServer(it) - } - } -// } - - builder.setSession(V2RayServiceManager.getRunningServerName()) - - 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) - builder.addDisallowedApplication(it) - else - builder.addAllowedApplication(it) - } catch (e: PackageManager.NameNotFoundException) { - Log.e(AppConfig.TAG, "Failed to configure app in VPN: ${e.localizedMessage}", e) - } - } - } else { - builder.addDisallowedApplication(selfPackageName) - } - - // Close the old interface since the parameters have been changed. - try { - mInterface.close() - } catch (ignored: Exception) { - // ignored - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - try { - connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to request default network", e) - } - } - - 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. - try { - mInterface = builder.establish()!! - isRunning = true - return true - } catch (e: Exception) { - // non-nullable lateinit var - Log.e(AppConfig.TAG, "Failed to establish VPN interface", e) - stopV2Ray() - } - return false - } - - /** - * Runs the tun2socks process. - * Starts the tun2socks process with the appropriate parameters. - */ - private fun runTun2socks() { - Log.i(AppConfig.TAG, "Start run $TUN2SOCKS") - val socksPort = SettingsManager.getSocksPort() - val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig() - val cmd = arrayListOf( - File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath, - "--netif-ipaddr", vpnConfig.ipv4Router, - "--netif-netmask", "255.255.255.252", - "--socks-server-addr", "$LOOPBACK:${socksPort}", - "--tunmtu", VPN_MTU.toString(), - "--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath, - "--enable-udprelay", - "--loglevel", "notice" - ) - - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) { - cmd.add("--netif-ip6addr") - cmd.add(vpnConfig.ipv6Router) - } - if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) { - val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt()) - cmd.add("--dnsgw") - cmd.add("$LOOPBACK:${localDnsPort}") - } - Log.i(AppConfig.TAG, cmd.toString()) - - try { - val proBuilder = ProcessBuilder(cmd) - proBuilder.redirectErrorStream(true) - process = proBuilder - .directory(applicationContext.filesDir) - .start() - Thread { - Log.i(AppConfig.TAG, "$TUN2SOCKS check") - process.waitFor() - Log.i(AppConfig.TAG, "$TUN2SOCKS exited") - if (isRunning) { - Log.i(AppConfig.TAG, "$TUN2SOCKS restart") - runTun2socks() - } - }.start() - Log.i(AppConfig.TAG, "$TUN2SOCKS process info : ${process.toString()}") - - sendFd() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to start $TUN2SOCKS process", e) - } - } - - /** - * Sends the file descriptor to the tun2socks process. - * Attempts to send the file descriptor multiple times if necessary. - */ - private fun sendFd() { - val fd = mInterface.fileDescriptor - val path = File(applicationContext.filesDir, "sock_path").absolutePath - Log.i(AppConfig.TAG, "LocalSocket path : $path") - - CoroutineScope(Dispatchers.IO).launch { - var tries = 0 - while (true) try { - Thread.sleep(50L shl tries) - Log.i(AppConfig.TAG, "LocalSocket sendFd tries: $tries") - LocalSocket().use { localSocket -> - localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM)) - localSocket.setFileDescriptorsForSend(arrayOf(fd)) - localSocket.outputStream.write(42) - } - break - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to send file descriptor, try: $tries", e) - if (tries > 5) break - tries += 1 - } - } - } - - /** - * Stops the V2Ray service. - * @param isForced Whether to force stop the service. - */ - private fun stopV2Ray(isForced: Boolean = true) { -// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "") -// val emptyInfo = VpnNetworkInfo() -// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo) -// saveVpnNetworkInfo(configName, info) - isRunning = false - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - try { - connectivity.unregisterNetworkCallback(defaultNetworkCallback) - } catch (ignored: Exception) { - // ignored - } - } - - try { - Log.i(AppConfig.TAG, "$TUN2SOCKS destroy") - process.destroy() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to destroy $TUN2SOCKS process", e) - } - - V2RayServiceManager.stopCoreLoop() - - if (isForced) { - //stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped - //It's strage but true. - //This can be verified by putting stopself() behind and call stopLoop and startLoop - //in a row for several times. You will find that later created v2ray core report port in use - //which means the first v2ray core somehow failed to stop and release the port. - stopSelf() - - try { - mInterface.close() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to close VPN interface", e) - } - } - } } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt new file mode 100644 index 00000000..66d8feb6 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt @@ -0,0 +1,53 @@ +package com.v2ray.ang.ui + +import android.content.Context +import android.os.Build +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import android.view.MenuItem +import androidx.annotation.RequiresApi +import com.v2ray.ang.util.MyContextWrapper +import com.v2ray.ang.R +import com.v2ray.ang.util.Utils + +abstract class BaseActivity : AppCompatActivity() { + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + android.R.id.home -> { + onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + checkDarkMode() + } + + private fun checkDarkMode() { + if (Utils.getDarkModeStatus(this)) { + if (this.javaClass.simpleName == "MainActivity") { + setTheme(R.style.AppThemeDark_NoActionBar) + } else { + setTheme(R.style.AppThemeDark) + } + } else { + if (this.javaClass.simpleName == "MainActivity") { + setTheme(R.style.AppThemeLight_NoActionBar) + } else { + setTheme(R.style.AppThemeLight) + } + } + } + + @RequiresApi(Build.VERSION_CODES.N) + override fun attachBaseContext(newBase: Context?) { + val context = newBase?.let { + MyContextWrapper.wrap(newBase, Utils.getLocale(newBase)) + } + super.attachBaseContext(context) + } + + + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/FragmentAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt similarity index 90% rename from V2rayNG/app/src/main/java/com/v2ray/ang/ui/FragmentAdapter.kt rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt index deeb43c3..d9d12992 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/FragmentAdapter.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt @@ -5,7 +5,7 @@ import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter class FragmentAdapter(fragmentActivity: FragmentActivity, private val mFragments: List) : - FragmentStateAdapter(fragmentActivity) { + FragmentStateAdapter(fragmentActivity) { override fun createFragment(position: Int): Fragment { return mFragments[position] 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 new file mode 100644 index 00000000..08d76490 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt @@ -0,0 +1,92 @@ +package com.v2ray.ang.ui + +import android.os.Handler +import android.os.Looper +import android.os.Bundle +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.GlobalScope +import kotlinx.coroutines.launch + +import java.io.IOException +import java.util.LinkedHashSet + +class LogcatActivity : BaseActivity() { + private lateinit var binding: ActivityLogcatBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityLogcatBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + + title = getString(R.string.title_logcat) + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + 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") + 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 = 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/MainActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt new file mode 100644 index 00000000..3a3e1e1c --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt @@ -0,0 +1,658 @@ +package com.v2ray.ang.ui + +import android.Manifest +import android.content.* +import android.net.Uri +import android.net.VpnService +import androidx.recyclerview.widget.LinearLayoutManager +import android.view.Menu +import android.view.MenuItem +import com.tbruyelle.rxpermissions.RxPermissions +import com.v2ray.ang.R +import android.os.Bundle +import android.text.TextUtils +import android.view.KeyEvent +import com.v2ray.ang.AppConfig +import android.content.res.ColorStateList +import com.google.android.material.navigation.NavigationView +import androidx.core.content.ContextCompat +import androidx.core.view.GravityCompat +import androidx.appcompat.app.ActionBarDrawerToggle +import androidx.recyclerview.widget.ItemTouchHelper +import android.util.Log +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.BuildConfig +import com.v2ray.ang.databinding.ActivityMainBinding +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.extension.toast +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import java.util.concurrent.TimeUnit +import com.v2ray.ang.helper.SimpleItemTouchHelperCallback +import com.v2ray.ang.service.V2RayServiceManager +import com.v2ray.ang.util.* +import com.v2ray.ang.viewmodel.MainViewModel +import kotlinx.coroutines.* +import me.drakeet.support.toast.ToastCompat +import java.io.File +import java.io.FileOutputStream + +class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener { + private lateinit var binding: ActivityMainBinding + + private val adapter by lazy { MainRecyclerAdapter(this) } + 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) } + private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + startV2Ray() + } + } + private var mItemTouchHelper: ItemTouchHelper? = null + val mainViewModel: MainViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + title = getString(R.string.title_server) + setSupportActionBar(binding.toolbar) + + binding.fab.setOnClickListener { + if (mainViewModel.isRunning.value == true) { + Utils.stopVService(this) + } else if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") { + val intent = VpnService.prepare(this) + if (intent == null) { + startV2Ray() + } else { + requestVpnPermission.launch(intent) + } + } else { + startV2Ray() + } + } + binding.layoutTest.setOnClickListener { + if (mainViewModel.isRunning.value == true) { + setTestState(getString(R.string.connection_test_testing)) + mainViewModel.testCurrentServerRealPing() + } else { +// tv_test_state.text = getString(R.string.connection_test_fail) + } + } + + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = adapter + + val callback = SimpleItemTouchHelperCallback(adapter) + mItemTouchHelper = ItemTouchHelper(callback) + mItemTouchHelper?.attachToRecyclerView(binding.recyclerView) + + + val toggle = ActionBarDrawerToggle( + this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) + binding.drawerLayout.addDrawerListener(toggle) + toggle.syncState() + binding.navView.setNavigationItemSelectedListener(this) + binding.version.text = "v${BuildConfig.VERSION_NAME} (${SpeedtestUtil.getLibVersion()})" + + setupViewModel() + copyAssets() + migrateLegacy() + } + + private fun setupViewModel() { + mainViewModel.updateListAction.observe(this) { index -> + if (index >= 0) { + adapter.notifyItemChanged(index) + } else { + adapter.notifyDataSetChanged() + } + } + mainViewModel.updateTestResultAction.observe(this) { setTestState(it) } + mainViewModel.isRunning.observe(this) { isRunning -> + adapter.isRunning = isRunning + if (isRunning) { + binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorSelected)) + setTestState(getString(R.string.connection_connected)) + binding.layoutTest.isFocusable = true + } else { + binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorUnselected)) + setTestState(getString(R.string.connection_not_connected)) + binding.layoutTest.isFocusable = false + } + hideCircle() + } + mainViewModel.startListenBroadcast() + } + + private fun copyAssets() { + val extFolder = Utils.userAssetPath(this) + lifecycleScope.launch(Dispatchers.IO) { + 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) + } + } + } + + private fun migrateLegacy() { + lifecycleScope.launch(Dispatchers.IO) { + val result = AngConfigManager.migrateLegacyConfig(this@MainActivity) + if (result != null) { + launch(Dispatchers.Main) { + if (result) { + toast(getString(R.string.migration_success)) + mainViewModel.reloadServerList() + } else { + toast(getString(R.string.migration_fail)) + } + } + } + } + } + + fun startV2Ray() { + if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) { + return + } + showCircle() +// toast(R.string.toast_services_start) + V2RayServiceManager.startV2Ray(this) + hideCircle() + } + + fun restartV2Ray() { + if (mainViewModel.isRunning.value == true) { + Utils.stopVService(this) + } + Observable.timer(500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + startV2Ray() + } + } + + public override fun onResume() { + super.onResume() + mainViewModel.reloadServerList() + } + + public override fun onPause() { + super.onPause() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.import_qrcode -> { + importQRcode(true) + true + } + R.id.import_clipboard -> { + importClipboard() + true + } + R.id.import_manually_vmess -> { + importManually(EConfigType.VMESS.value) + true + } + R.id.import_manually_vless -> { + importManually(EConfigType.VLESS.value) + true + } + R.id.import_manually_ss -> { + importManually(EConfigType.SHADOWSOCKS.value) + true + } + R.id.import_manually_socks -> { + importManually(EConfigType.SOCKS.value) + true + } + R.id.import_manually_trojan -> { + importManually(EConfigType.TROJAN.value) + true + } + R.id.import_config_custom_clipboard -> { + importConfigCustomClipboard() + true + } + R.id.import_config_custom_local -> { + importConfigCustomLocal() + true + } + R.id.import_config_custom_url -> { + importConfigCustomUrlClipboard() + true + } + R.id.import_config_custom_url_scan -> { + importQRcode(false) + true + } + +// R.id.sub_setting -> { +// startActivity() +// true +// } + + R.id.sub_update -> { + importConfigViaSub() + true + } + + R.id.export_all -> { + if (AngConfigManager.shareNonCustomConfigsToClipboard(this, mainViewModel.serverList) == 0) { + toast(R.string.toast_success) + } else { + toast(R.string.toast_failure) + } + true + } + + R.id.ping_all -> { + mainViewModel.testAllTcping() + true + } + + R.id.real_ping_all -> { + mainViewModel.testAllRealPing() + true + } + + R.id.service_restart -> { + restartV2Ray() + true + } + + R.id.del_all_config -> { + AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + MmkvManager.removeAllServer() + mainViewModel.reloadServerList() + } + .show() + true + } + + R.id.del_invalid_config -> { + AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + MmkvManager.removeInvalidServer() + mainViewModel.reloadServerList() + } + .show() + true + } + R.id.sort_by_test_results -> { + MmkvManager.sortByTestResults() + mainViewModel.reloadServerList() + true + } + R.id.filter_config -> { + mainViewModel.filterConfig(this) + true + } + + else -> super.onOptionsItemSelected(item) + } + + private fun importManually(createConfigType : Int) { + startActivity( + Intent() + .putExtra("createConfigType", createConfigType) + .putExtra("subscriptionId", mainViewModel.subscriptionId) + .setClass(this, ServerActivity::class.java) + ) + } + + /** + * import config from qrcode + */ + fun importQRcode(forConfig: 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(this) + .request(Manifest.permission.CAMERA) + .subscribe { + if (it) + if (forConfig) + scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java)) + else + scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java)) + else + toast(R.string.toast_permission_denied) + } +// } + return true + } + + private val scanQRCodeForConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + importBatchConfig(it.data?.getStringExtra("SCAN_RESULT")) + } + } + + private val scanQRCodeForUrlToCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + importConfigCustomUrl(it.data?.getStringExtra("SCAN_RESULT")) + } + } + + /** + * import config from clipboard + */ + fun importClipboard() + : Boolean { + try { + val clipboard = Utils.getClipboard(this) + importBatchConfig(clipboard) + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + fun importBatchConfig(server: String?, subid: String = "") { + val subid2 = if(subid.isNullOrEmpty()){ + mainViewModel.subscriptionId + }else{ + subid + } + val append = subid.isNullOrEmpty() + + var count = AngConfigManager.importBatchConfig(server, subid2, append) + if (count <= 0) { + count = AngConfigManager.importBatchConfig(Utils.decode(server!!), subid2, append) + } + if (count > 0) { + toast(R.string.toast_success) + mainViewModel.reloadServerList() + } else { + toast(R.string.toast_failure) + } + } + + fun importConfigCustomClipboard() + : Boolean { + try { + val configText = Utils.getClipboard(this) + if (TextUtils.isEmpty(configText)) { + toast(R.string.toast_none_data_clipboard) + return false + } + importCustomizeConfig(configText) + return true + } catch (e: Exception) { + e.printStackTrace() + return false + } + } + + /** + * import config from local config file + */ + fun importConfigCustomLocal(): Boolean { + try { + showFileChooser() + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + fun importConfigCustomUrlClipboard() + : Boolean { + try { + val url = Utils.getClipboard(this) + if (TextUtils.isEmpty(url)) { + toast(R.string.toast_none_data_clipboard) + return false + } + return importConfigCustomUrl(url) + } catch (e: Exception) { + e.printStackTrace() + return false + } + } + + /** + * import config from url + */ + fun importConfigCustomUrl(url: String?): Boolean { + try { + if (!Utils.isValidUrl(url)) { + toast(R.string.toast_invalid_url) + return false + } + lifecycleScope.launch(Dispatchers.IO) { + val configText = try { + Utils.getUrlContentWithCustomUserAgent(url) + } catch (e: Exception) { + e.printStackTrace() + "" + } + launch(Dispatchers.Main) { + importCustomizeConfig(configText) + } + } + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + /** + * import config from sub + */ + fun importConfigViaSub() + : Boolean { + try { + toast(R.string.title_sub_update) + MmkvManager.decodeSubscriptions().forEach { + if (TextUtils.isEmpty(it.first) + || TextUtils.isEmpty(it.second.remarks) + || TextUtils.isEmpty(it.second.url) + ) { + return@forEach + } + if (!it.second.enabled) { + return@forEach + } + val url = it.second.url + if (!Utils.isValidUrl(url)) { + return@forEach + } + Log.d(ANG_PACKAGE, url) + lifecycleScope.launch(Dispatchers.IO) { + val configText = try { + Utils.getUrlContentWithCustomUserAgent(url) + } catch (e: Exception) { + e.printStackTrace() + launch(Dispatchers.Main) { + toast("\"" + it.second.remarks + "\" " + getString(R.string.toast_failure)) + } + return@launch + } + launch(Dispatchers.Main) { + importBatchConfig(configText, it.first) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + /** + * show file chooser + */ + private fun showFileChooser() { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "*/*" + intent.addCategory(Intent.CATEGORY_OPENABLE) + + try { + chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) + } catch (ex: ActivityNotFoundException) { + toast(R.string.toast_require_file_manager) + } + } + + private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + val uri = it.data?.data + if (it.resultCode == RESULT_OK && uri != null) { + readContentFromUri(uri) + } + } + + /** + * read content from uri + */ + private fun readContentFromUri(uri: Uri) { + RxPermissions(this) + .request(Manifest.permission.READ_EXTERNAL_STORAGE) + .subscribe { + if (it) { + try { + contentResolver.openInputStream(uri).use { input -> + importCustomizeConfig(input?.bufferedReader()?.readText()) + } + } catch (e: Exception) { + e.printStackTrace() + } + } else + toast(R.string.toast_permission_denied) + } + } + + /** + * import customize config + */ + fun importCustomizeConfig(server: String?) { + try { + if (server == null || TextUtils.isEmpty(server)) { + toast(R.string.toast_none_data) + return + } + mainViewModel.appendCustomConfigServer(server) + mainViewModel.reloadServerList() + toast(R.string.toast_success) + //adapter.notifyItemInserted(mainViewModel.serverList.lastIndex) + } catch (e: Exception) { + ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show() + e.printStackTrace() + return + } + } + + fun setTestState(content: String?) { + binding.tvTestState.text = content + } + +// val mConnection = object : ServiceConnection { +// override fun onServiceDisconnected(name: ComponentName?) { +// } +// +// override fun onServiceConnected(name: ComponentName?, service: IBinder?) { +// sendMsg(AppConfig.MSG_REGISTER_CLIENT, "") +// } +// } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + moveTaskToBack(false) + return true + } + return super.onKeyDown(keyCode, event) + } + + fun showCircle() { + binding.fabProgressCircle.show() + } + + fun hideCircle() { + try { + Observable.timer(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + try { + if (binding.fabProgressCircle.isShown) { + binding.fabProgressCircle.hide() + } + } catch (e: Exception) { + Log.w(ANG_PACKAGE, e) + } + } + } catch (e: Exception) { + Log.d(ANG_PACKAGE, e.toString()) + } + } + + override fun onBackPressed() { + if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { + binding.drawerLayout.closeDrawer(GravityCompat.START) + } else { + super.onBackPressed() + } + } + + override fun onNavigationItemSelected(item: MenuItem): Boolean { + // Handle navigation view item clicks here. + when (item.itemId) { + //R.id.server_profile -> activityClass = MainActivity::class.java + R.id.sub_setting -> { + startActivity(Intent(this, SubSettingActivity::class.java)) + } + R.id.settings -> { + startActivity(Intent(this, SettingsActivity::class.java) + .putExtra("isRunning", mainViewModel.isRunning.value == true)) + } + R.id.user_asset_setting -> { + startActivity(Intent(this, UserAssetActivity::class.java)) + } + R.id.feedback -> { + Utils.openUri(this, AppConfig.v2rayNGIssues) + } + R.id.promotion -> { + Utils.openUri(this, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}") + } + R.id.logcat -> { + startActivity(Intent(this, LogcatActivity::class.java)) + } + } + binding.drawerLayout.closeDrawer(GravityCompat.START) + return true + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt new file mode 100644 index 00000000..310d3af7 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt @@ -0,0 +1,260 @@ +package com.v2ray.ang.ui + +import android.content.Intent +import android.graphics.Color +import android.text.TextUtils +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import com.google.gson.Gson +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +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.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 rx.Observable +import rx.android.schedulers.AndroidSchedulers +import java.util.concurrent.TimeUnit + +class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter() + , ItemTouchHelperAdapter { + companion object { + private const val VIEW_TYPE_ITEM = 1 + private const val VIEW_TYPE_FOOTER = 2 + } + + private var mActivity: MainActivity = activity + private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) } + private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) } + private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } + private val share_method: Array by lazy { + mActivity.resources.getStringArray(R.array.share_method) + } + var isRunning = false + + override fun getItemCount() = mActivity.mainViewModel.serversCache.size + 1 + + override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { + if (holder is MainViewHolder) { + val guid = mActivity.mainViewModel.serversCache[position].guid + val config = mActivity.mainViewModel.serversCache[position].config +// //filter +// if (mActivity.mainViewModel.subscriptionId.isNotEmpty() +// && mActivity.mainViewModel.subscriptionId != config.subscriptionId +// ) { +// holder.itemMainBinding.cardView.visibility = View.GONE +// } else { +// holder.itemMainBinding.cardView.visibility = View.VISIBLE +// } + + val outbound = config.getProxyOutbound() + val aff = MmkvManager.decodeServerAffiliationInfo(guid) + + holder.itemMainBinding.tvName.text = config.remarks + holder.itemView.setBackgroundColor(Color.TRANSPARENT) + holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString() ?: "" + if ((aff?.testDelayMillis ?: 0L) < 0L) { + holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed)) + } else { + holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing)) + } + if (guid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) { + holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorSelected) + } else { + holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorUnselected) + } + holder.itemMainBinding.tvSubscription.text = "" + val json = subStorage?.decodeString(config.subscriptionId) + if (!json.isNullOrBlank()) { + val sub = Gson().fromJson(json, SubscriptionItem::class.java) + holder.itemMainBinding.tvSubscription.text = sub.remarks + } + + var shareOptions = share_method.asList() + when (config.configType) { + EConfigType.CUSTOM -> { + holder.itemMainBinding.tvType.text = mActivity.getString(R.string.server_customize_config) + shareOptions = shareOptions.takeLast(1) + } + EConfigType.VLESS -> { + holder.itemMainBinding.tvType.text = config.configType.name + } + else -> { + holder.itemMainBinding.tvType.text = config.configType.name.lowercase() + } + } + holder.itemMainBinding.tvStatistics.text = "${outbound?.getServerAddress()} : ${outbound?.getServerPort()}" + + holder.itemMainBinding.layoutShare.setOnClickListener { + AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i -> + try { + when (i) { + 0 -> { + if (config.configType == EConfigType.CUSTOM) { + shareFullContent(guid) + } else { + val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity)) + ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid)) + AlertDialog.Builder(mActivity).setView(ivBinding.root).show() + } + } + 1 -> { + if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) { + mActivity.toast(R.string.toast_success) + } else { + mActivity.toast(R.string.toast_failure) + } + } + 2 -> shareFullContent(guid) + else -> mActivity.toast("else") + } + } catch (e: Exception) { + e.printStackTrace() + } + }.show() + } + + holder.itemMainBinding.layoutEdit.setOnClickListener { + val intent = Intent().putExtra("guid", guid) + .putExtra("isRunning", isRunning) + if (config.configType == EConfigType.CUSTOM) { + mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java)) + } else { + mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java)) + } + } + holder.itemMainBinding.layoutRemove.setOnClickListener { + if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) { + if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { + AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + removeServer(guid, position) + } + .show() + } else { + removeServer(guid, position) + } + } + } + + holder.itemMainBinding.infoContainer.setOnClickListener { + val selected = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) + if (guid != selected) { + mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid) + if (!TextUtils.isEmpty(selected)) { + notifyItemChanged(mActivity.mainViewModel.getPosition(selected!!)) + } + notifyItemChanged(mActivity.mainViewModel.getPosition(guid)) + if (isRunning) { + mActivity.showCircle() + Utils.stopVService(mActivity) + Observable.timer(500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + V2RayServiceManager.startV2Ray(mActivity) + mActivity.hideCircle() + } + } + } + } + } + if (holder is FooterViewHolder) { + //if (activity?.defaultDPreference?.getPrefBoolean(AppConfig.PREF_INAPP_BUY_IS_PREMIUM, false)) { + if (true) { + holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE + } else { + holder.itemFooterBinding.layoutEdit.setOnClickListener { + Utils.openUri(mActivity, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}") + } + } + } + } + + private fun shareFullContent(guid: String) { + if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) { + mActivity.toast(R.string.toast_success) + } else { + mActivity.toast(R.string.toast_failure) + } + } + + private fun removeServer(guid: String,position:Int) { + mActivity.mainViewModel.removeServer(guid) + notifyItemRemoved(position) + notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { + return when (viewType) { + VIEW_TYPE_ITEM -> + MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + else -> + FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + } + } + + override fun getItemViewType(position: Int): Int { + return if (position == mActivity.mainViewModel.serversCache.size) { + VIEW_TYPE_FOOTER + } else { + VIEW_TYPE_ITEM + } + } + + open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + fun onItemSelected() { + itemView.setBackgroundColor(Color.LTGRAY) + } + + fun onItemClear() { + itemView.setBackgroundColor(0) + } + } + + class MainViewHolder(val itemMainBinding: ItemRecyclerMainBinding) : + BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder + + class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) : + BaseViewHolder(itemFooterBinding.root), ItemTouchHelperViewHolder + + override fun onItemDismiss(position: Int) { + val guid = mActivity.mainViewModel.serversCache.getOrNull(position)?.guid ?: return + if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) { +// mActivity.alert(R.string.del_config_comfirm) { +// positiveButton(android.R.string.ok) { + mActivity.mainViewModel.removeServer(guid) + notifyItemRemoved(position) +// } +// show() +// } + } + } + + override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { + mActivity.mainViewModel.swapServer(fromPosition, toPosition) + notifyItemMoved(fromPosition, toPosition) + // position is changed, since position is used by click callbacks, need to update range + if (toPosition > fromPosition) + notifyItemRangeChanged(fromPosition, toPosition - fromPosition + 1) + else + notifyItemRangeChanged(toPosition, fromPosition - toPosition + 1) + return true + } + + override fun onItemMoveCompleted() { + // do nothing + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt new file mode 100644 index 00000000..eabed93d --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt @@ -0,0 +1,355 @@ +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 androidx.appcompat.widget.SearchView +import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.R +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.util.AppManagerUtil +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import java.text.Collator +import java.util.* + +class PerAppProxyActivity : BaseActivity() { + private lateinit var binding: ActivityBypassListBinding + + private var adapter: PerAppProxyAdapter? = null + private var appsAll: List? = null + private val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityBypassListBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL) + binding.recyclerView.addItemDecoration(dividerItemDecoration) + + val blacklist = defaultSharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, null) + + AppManagerUtil.rxLoadNetworkAppList(this) + .subscribeOn(Schedulers.io()) + .map { + if (blacklist != null) { + it.forEach { one -> + if ((blacklist.contains(one.packageName))) { + one.isSelected = 1 + } else { + one.isSelected = 0 + } + } + val comparator = Comparator { p1, p2 -> + when { + p1.isSelected > p2.isSelected -> -1 + p1.isSelected == p2.isSelected -> 0 + else -> 1 + } + } + it.sortedWith(comparator) + } else { + val comparator = object : Comparator { + val collator = Collator.getInstance() + override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName) + } + it.sortedWith(comparator) + } + } +// .map { +// val comparator = object : Comparator { +// val collator = Collator.getInstance() +// override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName) +// } +// it.sortedWith(comparator) +// } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + appsAll = it + adapter = PerAppProxyAdapter(this, it, blacklist) + binding.recyclerView.adapter = adapter + binding.pbWaiting.visibility = View.GONE + } + /*** + recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() { + var dst = 0 + val threshold = resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 2 + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + dst += dy + if (dst > threshold) { + header_view.hide() + dst = 0 + } else if (dst < -20) { + header_view.show() + dst = 0 + } + } + + var hiding = false + fun View.hide() { + val target = -height.toFloat() + if (hiding || translationY == target) return + animate() + .translationY(target) + .setInterpolator(AccelerateInterpolator(2F)) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + hiding = false + } + }) + hiding = true + } + + var showing = false + fun View.show() { + val target = 0f + if (showing || translationY == target) return + animate() + .translationY(target) + .setInterpolator(DecelerateInterpolator(2F)) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + showing = false + } + }) + showing = true + } + }) + ***/ + + binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked -> + defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_PER_APP_PROXY, isChecked).apply() + } + binding.switchPerAppProxy.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_PER_APP_PROXY, false) + + binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked -> + defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_BYPASS_APPS, isChecked).apply() + } + binding.switchBypassApps.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_BYPASS_APPS, false) + + /*** + et_search.setOnEditorActionListener { v, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + //hide + var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS) + + val key = v.text.toString().toUpperCase() + val apps = ArrayList() + if (TextUtils.isEmpty(key)) { + appsAll?.forEach { + apps.add(it) + } + } else { + appsAll?.forEach { + if (it.appName.toUpperCase().indexOf(key) >= 0) { + apps.add(it) + } + } + } + adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist) + recycler_view.adapter = adapter + adapter?.notifyDataSetChanged() + true + } else { + false + } + } + ***/ + } + + override fun onPause() { + super.onPause() + adapter?.let { + defaultSharedPreferences.edit().putStringSet(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist).apply() + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_bypass_list, 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 { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + filterProxyApp(newText!!) + return false + } + }) + } + + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.select_all -> adapter?.let { + val pkgNames = it.apps.map { it.packageName } + if (it.blacklist.containsAll(pkgNames)) { + it.apps.forEach { + val packageName = it.packageName + adapter?.blacklist!!.remove(packageName) + } + } else { + it.apps.forEach { + val packageName = it.packageName + adapter?.blacklist!!.add(packageName) + } + } + it.notifyDataSetChanged() + true + } ?: false + R.id.select_proxy_app -> { + selectProxyApp() + true + } + R.id.import_proxy_app -> { + importProxyApp() + true + } + R.id.export_proxy_app -> { + exportProxyApp() + true + } + else -> super.onOptionsItemSelected(item) + } + + private fun selectProxyApp() { + toast(R.string.msg_downloading_content) + val url = AppConfig.androidpackagenamelistUrl + lifecycleScope.launch(Dispatchers.IO) { + val content = Utils.getUrlContext(url, 5000) + launch(Dispatchers.Main) { + Log.d(ANG_PACKAGE, content) + selectProxyApp(content, true) + toast(R.string.toast_success) + } + } + } + + private fun importProxyApp() { + val content = Utils.getClipboard(applicationContext) + if (TextUtils.isEmpty(content)) { + return + } + selectProxyApp(content, false) + toast(R.string.toast_success) + } + + private fun exportProxyApp() { + var lst = binding.switchBypassApps.isChecked.toString() + + adapter?.blacklist?.forEach block@{ + lst = lst + System.getProperty("line.separator") + it + } + Utils.setClipboard(applicationContext, lst) + toast(R.string.toast_success) + } + + private fun selectProxyApp(content: String, force: Boolean): Boolean { + try { + val proxyApps = if (TextUtils.isEmpty(content)) { + Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt") + } else { + content + } + if (TextUtils.isEmpty(proxyApps)) { + return false + } + + adapter?.blacklist!!.clear() + + if (binding.switchBypassApps.isChecked) { + adapter?.let { + it.apps.forEach block@{ + val packageName = it.packageName + Log.d(ANG_PACKAGE, packageName) + if (!inProxyApps(proxyApps, packageName, force)) { + adapter?.blacklist!!.add(packageName) + println(packageName) + return@block + } + } + it.notifyDataSetChanged() + } + } else { + adapter?.let { + it.apps.forEach block@{ + val packageName = it.packageName + Log.d(ANG_PACKAGE, packageName) + if (inProxyApps(proxyApps, packageName, force)) { + adapter?.blacklist!!.add(packageName) + println(packageName) + return@block + } + } + it.notifyDataSetChanged() + } + } + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + private fun inProxyApps(proxyApps: String, packageName: String, force: Boolean): Boolean { + if (force) { + if (packageName == "com.google.android.webview") { + return false + } + if (packageName.startsWith("com.google")) { + return true + } + } + + return proxyApps.indexOf(packageName) >= 0 + } + + private fun filterProxyApp(content: String): Boolean { + val apps = ArrayList() + + val key = content.uppercase() + if (key.isNotEmpty()) { + appsAll?.forEach { + if (it.appName.uppercase().indexOf(key) >= 0 + || it.packageName.uppercase().indexOf(key) >= 0) { + apps.add(it) + } + } + } else { + appsAll?.forEach { + apps.add(it) + } + } + + adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist) + binding.recyclerView.adapter = adapter + adapter?.notifyDataSetChanged() + return true + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt similarity index 79% rename from V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyAdapter.kt rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt index 1d02b3b1..040e9b9c 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyAdapter.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt @@ -1,14 +1,16 @@ package com.v2ray.ang.ui import android.view.LayoutInflater +import androidx.recyclerview.widget.RecyclerView import android.view.View import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView +import com.v2ray.ang.R import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding import com.v2ray.ang.dto.AppInfo +import java.util.* class PerAppProxyAdapter(val activity: BaseActivity, val apps: List, blacklist: MutableSet?) : - RecyclerView.Adapter() { + RecyclerView.Adapter() { companion object { private const val VIEW_TYPE_HEADER = 0 @@ -32,10 +34,8 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List, bl return when (viewType) { VIEW_TYPE_HEADER -> { val view = View(ctx) - view.layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - 0 - ) + view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 0) BaseViewHolder(view) } // VIEW_TYPE_ITEM -> AppViewHolder(ctx.layoutInflater @@ -51,30 +51,29 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List, bl open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) inner class AppViewHolder(private val itemBypassBinding: ItemRecyclerBypassListBinding) : BaseViewHolder(itemBypassBinding.root), - View.OnClickListener { + View.OnClickListener { private val inBlacklist: Boolean get() = blacklist.contains(appInfo.packageName) private lateinit var appInfo: AppInfo fun bind(appInfo: AppInfo) { this.appInfo = appInfo - // Set app icon and name itemBypassBinding.icon.setImageDrawable(appInfo.appIcon) - itemBypassBinding.name.text = if (appInfo.isSystemApp) { - String.format("** %s", appInfo.appName) +// 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 } else { - appInfo.appName + itemBypassBinding.name.text = appInfo.appName + //name.textColor = Color.DKGRAY } - // 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/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt new file mode 100644 index 00000000..030e80ff --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt @@ -0,0 +1,38 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import com.v2ray.ang.R +import androidx.fragment.app.Fragment +import com.google.android.material.tabs.TabLayoutMediator +import com.v2ray.ang.AppConfig +import com.v2ray.ang.databinding.ActivityRoutingSettingsBinding + +class RoutingSettingsActivity : BaseActivity() { + private lateinit var binding: ActivityRoutingSettingsBinding + + private val titles: Array by lazy { + resources.getStringArray(R.array.routing_tag) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityRoutingSettingsBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + + title = getString(R.string.title_pref_routing_custom) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + 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 new file mode 100644 index 00000000..f5a8e395 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsFragment.kt @@ -0,0 +1,158 @@ +package com.v2ray.ang.ui + +import android.Manifest +import android.app.Activity.RESULT_OK +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils +import android.view.* +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager +import com.tbruyelle.rxpermissions.RxPermissions +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.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +class RoutingSettingsFragment : Fragment() { + private lateinit var binding: FragmentRoutingSettingsBinding + companion object { + private const val routing_arg = "routing_arg" + } + + val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()) } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + // Inflate the layout for this fragment + binding = FragmentRoutingSettingsBinding.inflate(layoutInflater) + 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 = defaultSharedPreferences.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() + defaultSharedPreferences.edit().putString(requireArguments().getString(routing_arg), content).apply() + 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_AGENT + } + 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/ScScannerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt new file mode 100644 index 00000000..da1cb7cd --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt @@ -0,0 +1,45 @@ +package com.v2ray.ang.ui + +import android.Manifest +import android.content.* +import com.tbruyelle.rxpermissions.RxPermissions +import com.v2ray.ang.R +import com.v2ray.ang.util.AngConfigManager +import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContracts +import com.v2ray.ang.extension.toast + +class ScScannerActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_none) + importQRcode() + } + + fun importQRcode(): Boolean { + RxPermissions(this) + .request(Manifest.permission.CAMERA) + .subscribe { + if (it) + scanQRCode.launch(Intent(this, ScannerActivity::class.java)) + else + toast(R.string.toast_permission_denied) + } + + return true + } + + private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + val count = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false) + if (count > 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/java/com/v2ray/ang/ui/ScSwitchActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt similarity index 69% rename from V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScSwitchActivity.kt rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt index 0495318a..3d3101ef 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScSwitchActivity.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt @@ -1,7 +1,8 @@ package com.v2ray.ang.ui -import android.os.Bundle import com.v2ray.ang.R +import com.v2ray.ang.util.Utils +import android.os.Bundle import com.v2ray.ang.service.V2RayServiceManager class ScSwitchActivity : BaseActivity() { @@ -11,10 +12,10 @@ class ScSwitchActivity : BaseActivity() { setContentView(R.layout.activity_none) - if (V2RayServiceManager.isRunning()) { - V2RayServiceManager.stopVService(this) + if (V2RayServiceManager.v2rayPoint.isRunning) { + Utils.stopVService(this) } else { - V2RayServiceManager.startVServiceFromToggle(this) + Utils.startVServiceFromToggle(this) } finish() } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt new file mode 100644 index 00000000..bc3785ec --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt @@ -0,0 +1,116 @@ +package com.v2ray.ang.ui + +import android.Manifest +import android.app.Activity +import android.os.Bundle +import com.google.zxing.Result +import me.dm7.barcodescanner.zxing.ZXingScannerView +import android.content.Intent +import android.graphics.BitmapFactory +import android.view.Menu +import android.view.MenuItem +import androidx.activity.result.contract.ActivityResultContracts +import com.google.zxing.BarcodeFormat +import com.tbruyelle.rxpermissions.RxPermissions +import com.v2ray.ang.R +import com.v2ray.ang.extension.toast +import com.v2ray.ang.util.QRCodeDecoder + +class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler { + + private var mScannerView: ZXingScannerView? = null + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + mScannerView = ZXingScannerView(this) // Programmatically initialize the scanner view + + mScannerView?.setAutoFocus(true) + val formats = ArrayList() + formats.add(BarcodeFormat.QR_CODE) + mScannerView?.setFormats(formats) + + setContentView(mScannerView) // Set the scanner view as the content view + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + public override fun onResume() { + super.onResume() + mScannerView!!.setResultHandler(this) // Register ourselves as a handler for scan results. + mScannerView!!.startCamera() // Start camera on resume + } + + public override fun onPause() { + super.onPause() + mScannerView!!.stopCamera() // Stop camera on pause + } + + override fun handleResult(rawResult: Result) { + // Do something with the result here +// Log.v(FragmentActivity.TAG, rawResult.text) // Prints scan results +// Log.v(FragmentActivity.TAG, rawResult.barcodeFormat.toString()) // Prints the scan format (qrcode, pdf417 etc.) + + finished(rawResult.text) + + // If you would like to resume scanning, call this method below: +// mScannerView!!.resumeCameraPreview(this) + } + + private fun finished(text: String) { + val intent = Intent() + intent.putExtra("SCAN_RESULT", text) + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_scanner, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.select_photo -> { + RxPermissions(this) + .request(Manifest.permission.READ_EXTERNAL_STORAGE) + .subscribe { + if (it) { + try { + showFileChooser() + } catch (e: Exception) { + e.printStackTrace() + } + } else + toast(R.string.toast_permission_denied) + } + true + } + else -> super.onOptionsItemSelected(item) + } + + private fun showFileChooser() { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "image/*" + intent.addCategory(Intent.CATEGORY_OPENABLE) + //intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + + try { + chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) + } catch (ex: android.content.ActivityNotFoundException) { + toast(R.string.toast_require_file_manager) + } + } + + private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + val uri = it.data?.data + if (it.resultCode == RESULT_OK && uri != null) { + try { + val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri)) + val text = QRCodeDecoder.syncDecodeQRCode(bitmap) + finished(text!!) + } catch (e: Exception) { + e.printStackTrace() + toast(e.message.toString()) + } + } + } +} 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 new file mode 100644 index 00000000..7fc1973b --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt @@ -0,0 +1,412 @@ +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.* +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.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.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 + +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 et_sni: EditText? by lazy { findViewById(R.id.et_sni) } + private val sp_stream_fingerprint: Spinner? by lazy { findViewById(R.id.sp_stream_fingerprint) } //uTLS + 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 et_request_host: EditText? by lazy { findViewById(R.id.et_request_host) } + 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 + + 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) + } + 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]) + } + } + override fun onNothingSelected(parent: AdapterView<*>?) { + // do nothing + } + } + if (config != null) { + bindingServer(config) + } else { + clearServer() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + /** + * bingding seleced server config + */ + private fun bindingServer(config: ServerConfig): Boolean { + val outbound = config.getProxyOutbound() ?: return false + val streamSetting = config.outboundBean?.streamSettings ?: 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.TROJAN) { + val flow = Utils.arrayFind(flows, outbound.settings?.servers?.get(0)?.flow.orEmpty()) + if (flow >= 0) { + sp_flow?.setSelection(flow) + } + } + 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 streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security) + if (streamSecurity >= 0) { + sp_stream_security?.setSelection(streamSecurity) + (streamSetting.tlsSettings?: streamSetting.xtlsSettings)?.let { tlsSetting -> + val allowinsecure = Utils.arrayFind(allowinsecures, tlsSetting.allowInsecure.toString()) + if (allowinsecure >= 0) { + sp_allow_insecure?.setSelection(allowinsecure) + } + 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())!!) + sp_stream_alpn?.setSelection(alpnIndex) + } + + } + } + 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) + 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())) { + 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) + } + config.outboundBean?.streamSettings?.let { + saveStreamSettings(it) + } + if(config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) { + config.subscriptionId = subscriptionId!! + } + + 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() + server.flow = + if (streamSecuritys[sp_stream_security?.selectedItemPosition ?: 0] == V2rayConfig.XTLS) { + flows[sp_flow?.selectedItemPosition ?: 0] + } else { + "" + } + } + } + + 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 + var utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: return + var alpnIndex = sp_stream_alpn?.selectedItemPosition ?: 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 + ) + if (sniField.isNotBlank()) { + sni = sniField + } + val allowInsecure = if (allowinsecures[allowInsecureField].isBlank()) { + settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false + } else { + allowinsecures[allowInsecureField].toBoolean() + } + + streamSetting.populateTlsSettings(streamSecuritys[streamSecurity], allowInsecure, sni, uTlsItems[utlsIndex], alpns[alpnIndex]) + } + + private fun transportTypes(network: String?): Array { + return if (network == "tcp") { + tcpTypes + } else if (network == "kcp" || network == "quic") { + kcpAndQuicTypes + } else if (network == "grpc") { + grpcModes + } else { + arrayOf("---") + } + } + + /** + * save server config + */ + private fun deleteServer(): Boolean { + if (editGuid.isNotEmpty()) { + if (editGuid != mainStorage?.decodeString(MmkvManager.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() + } + .show() + } else { + MmkvManager.removeServer(editGuid) + finish() + } + } + } + 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/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerCustomConfigActivity.kt similarity index 59% rename from V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerCustomConfigActivity.kt index 44e0b32b..c8138915 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerCustomConfigActivity.kt @@ -2,41 +2,42 @@ 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.widget.Toast import androidx.appcompat.app.AlertDialog -import com.blacksquircle.ui.editorkit.utils.EditorTheme import com.blacksquircle.ui.language.json.JsonLanguage -import com.v2ray.ang.AppConfig +import com.google.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.ProfileItem +import com.v2ray.ang.dto.ServerConfig +import com.v2ray.ang.dto.V2rayConfig import com.v2ray.ang.extension.toast -import com.v2ray.ang.extension.toastSuccess -import com.v2ray.ang.fmt.CustomFmt -import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.util.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 lateinit var binding: ActivityServerCustomConfigBinding + 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 == MmkvManager.getSelectServer() + && editGuid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(binding.root) + binding = ActivityServerCustomConfigBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) title = getString(R.string.title_server) - if (!Utils.getDarkModeStatus(this)) { - binding.editor.colorScheme = EditorTheme.INTELLIJ_LIGHT - } binding.editor.language = JsonLanguage() val config = MmkvManager.decodeServerConfig(editGuid) if (config != null) { @@ -44,17 +45,20 @@ class ServerCustomConfigActivity : BaseActivity() { } else { clearServer() } + supportActionBar?.setDisplayHomeAsUpEnabled(true) } /** - * Binding selected server config + * bingding seleced server config */ - private fun bindingServer(config: ProfileItem): Boolean { + private fun bindingServer(config: ServerConfig): Boolean { binding.etRemarks.text = Utils.getEditable(config.remarks) - val raw = MmkvManager.decodeServerRaw(editGuid) - val configContent = raw.orEmpty() - - binding.editor.setTextContent(Utils.getEditable(configContent)) + val raw = serverRawStorage?.decodeString(editGuid) + if (raw.isNullOrBlank()) { + binding.editor.setTextContent(Utils.getEditable(config.fullConfig?.toPrettyPrinting().orEmpty())) + } else { + binding.editor.setTextContent(Utils.getEditable(raw)) + } return true } @@ -75,24 +79,21 @@ class ServerCustomConfigActivity : BaseActivity() { return false } - val profileItem = try { - CustomFmt.parse(binding.editor.text.toString()) + val v2rayConfig = try { + Gson().fromJson(binding.editor.text.toString(), V2rayConfig::class.java) } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to parse custom configuration", e) - toast("${getString(R.string.toast_malformed_josn)} ${e.cause?.message}") + 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) ?: 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 + val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM) + config.remarks = binding.etRemarks.text.toString().trim() + config.fullConfig = v2rayConfig MmkvManager.encodeServerConfig(editGuid, config) - MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString()) - toastSuccess(R.string.toast_success) + serverRawStorage?.encode(editGuid, binding.editor.text.toString()) + toast(R.string.toast_success) finish() return true } @@ -103,14 +104,11 @@ class ServerCustomConfigActivity : BaseActivity() { private fun deleteServer(): Boolean { if (editGuid.isNotEmpty()) { 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() + .setPositiveButton(android.R.string.ok) { _, _ -> + MmkvManager.removeServer(editGuid) + finish() + } + .show() } return true } @@ -137,12 +135,10 @@ class ServerCustomConfigActivity : BaseActivity() { deleteServer() true } - R.id.save_config -> { saveServer() true } - else -> super.onOptionsItemSelected(item) } } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt new file mode 100644 index 00000000..5284cc64 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt @@ -0,0 +1,182 @@ +package com.v2ray.ang.ui + +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils +import android.view.View +import androidx.activity.viewModels +import androidx.preference.* +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.util.Utils +import com.v2ray.ang.viewmodel.SettingsViewModel + +class SettingsActivity : BaseActivity() { + private val settingsViewModel: SettingsViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + + title = getString(R.string.title_settings) + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + settingsViewModel.startListenPreferenceChange() + } + + class SettingsFragment : PreferenceFragmentCompat() { + 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 localDnsPort by lazy { findPreference(AppConfig.PREF_LOCAL_DNS_PORT) } + private val vpnDns by lazy { findPreference(AppConfig.PREF_VPN_DNS) } + // val autoRestart by lazy { findPreference(PREF_AUTO_RESTART) as CheckBoxPreference } + private val remoteDns by lazy { findPreference(AppConfig.PREF_REMOTE_DNS) } + private val domesticDns by lazy { findPreference(AppConfig.PREF_DOMESTIC_DNS) } + private val socksPort by lazy { findPreference(AppConfig.PREF_SOCKS_PORT) } + private val httpPort by lazy { findPreference(AppConfig.PREF_HTTP_PORT) } + private val routingCustom by lazy { findPreference(AppConfig.PREF_ROUTING_CUSTOM) } + // val licenses: Preference by lazy { findPreference(PREF_LICENSES) } +// val feedback: Preference by lazy { findPreference(PREF_FEEDBACK) } +// val tgGroup: Preference by lazy { findPreference(PREF_TG_GROUP) } + + private val mode by lazy { findPreference(AppConfig.PREF_MODE) } + + override fun onCreatePreferences(bundle: Bundle?, s: String?) { + addPreferencesFromResource(R.xml.pref_settings) + + routingCustom?.setOnPreferenceClickListener { + startActivity(Intent(activity, RoutingSettingsActivity::class.java)) + false + } + +// licenses.onClick { +// val fragment = LicensesDialogFragment.Builder(act) +// .setNotices(R.raw.licenses) +// .setIncludeOwnLicense(false) +// .build() +// fragment.show((act as AppCompatActivity).supportFragmentManager, null) +// } +// +// feedback.onClick { +// Utils.openUri(activity, "https://github.com/2dust/v2rayNG/issues") +// } +// tgGroup.onClick { +// // Utils.openUri(activity, "https://t.me/v2rayN") +// val intent = Intent(Intent.ACTION_VIEW, Uri.parse("tg:resolve?domain=v2rayN")) +// try { +// startActivity(intent) +// } catch (e: Exception) { +// e.printStackTrace() +// toast(R.string.toast_tg_app_not_found) +// } +// } + + perAppProxy?.setOnPreferenceClickListener { + startActivity(Intent(activity, PerAppProxyActivity::class.java)) + perAppProxy?.isChecked = true + false + } + + remoteDns?.setOnPreferenceChangeListener { _, any -> + // remoteDns.summary = any as String + val nval = any as String + remoteDns?.summary = if (nval == "") AppConfig.DNS_AGENT else nval + true + } + domesticDns?.setOnPreferenceChangeListener { _, any -> + // domesticDns.summary = any as String + val nval = any as String + domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval + true + } + + localDns?.setOnPreferenceChangeListener{ _, any -> + updateLocalDns(any as Boolean) + true + } + localDnsPort?.setOnPreferenceChangeListener { _, any -> + val nval = any as String + localDnsPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval + true + } + vpnDns?.setOnPreferenceChangeListener { _, any -> + vpnDns?.summary = any as String + true + } + socksPort?.setOnPreferenceChangeListener { _, any -> + val nval = any as String + 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 + } + mode?.setOnPreferenceChangeListener { _, newValue -> + updateMode(newValue.toString()) + true + } + mode?.dialogLayoutResource = R.layout.preference_with_help_link + //loglevel.summary = "LogLevel" + } + + override fun onStart() { + super.onStart() + val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()) + updateMode(defaultSharedPreferences.getString(AppConfig.PREF_MODE, "VPN")) + var remoteDnsString = defaultSharedPreferences.getString(AppConfig.PREF_REMOTE_DNS, "") + domesticDns?.summary = defaultSharedPreferences.getString(AppConfig.PREF_DOMESTIC_DNS, "") + + localDnsPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS) + socksPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS) + httpPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP) + + if (TextUtils.isEmpty(remoteDnsString)) { + remoteDnsString = AppConfig.DNS_AGENT + } + if (TextUtils.isEmpty(domesticDns?.summary)) { + domesticDns?.summary = AppConfig.DNS_DIRECT + } + remoteDns?.summary = remoteDnsString + vpnDns?.summary = defaultSharedPreferences.getString(AppConfig.PREF_VPN_DNS, remoteDnsString) + + if (TextUtils.isEmpty(localDnsPort?.summary)) { + localDnsPort?.summary = AppConfig.PORT_LOCAL_DNS + } + if (TextUtils.isEmpty(socksPort?.summary)) { + socksPort?.summary = AppConfig.PORT_SOCKS + } + if (TextUtils.isEmpty(httpPort?.summary)) { + httpPort?.summary = AppConfig.PORT_HTTP + } + } + + private fun updateMode(mode: String?) { + val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()) + val vpn = mode == "VPN" + perAppProxy?.isEnabled = vpn + perAppProxy?.isChecked = PreferenceManager.getDefaultSharedPreferences(requireActivity()) + .getBoolean(AppConfig.PREF_PER_APP_PROXY, false) + localDns?.isEnabled = vpn + fakeDns?.isEnabled = vpn + localDnsPort?.isEnabled = vpn + vpnDns?.isEnabled = vpn + if (vpn) { + updateLocalDns(defaultSharedPreferences.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)) + } + } + + private fun updateLocalDns(enabled: Boolean) { + fakeDns?.isEnabled = enabled + localDnsPort?.isEnabled = enabled + vpnDns?.isEnabled = !enabled + } + } + + fun onModeHelpClicked(view: View) { + Utils.openUri(this, AppConfig.v2rayNGWikiMode) + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt new file mode 100644 index 00000000..5a5a8d44 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt @@ -0,0 +1,133 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +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.ActivitySubEditBinding +import com.v2ray.ang.dto.SubscriptionItem +import com.v2ray.ang.extension.toast +import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.util.Utils + +class SubEditActivity : BaseActivity() { + private lateinit var binding: ActivitySubEditBinding + + 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?) { + super.onCreate(savedInstanceState) + binding = ActivitySubEditBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + title = getString(R.string.title_sub_setting) + + val json = subStorage?.decodeString(editSubId) + if (!json.isNullOrBlank()) { + bindingServer(Gson().fromJson(json, SubscriptionItem::class.java)) + } else { + clearServer() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + /** + * bingding seleced server config + */ + private fun bindingServer(subItem: SubscriptionItem): Boolean { + binding.etRemarks.text = Utils.getEditable(subItem.remarks) + binding.etUrl.text = Utils.getEditable(subItem.url) + binding.chkEnable.isChecked = subItem.enabled + return true + } + + /** + * clear or init server config + */ + private fun clearServer(): Boolean { + binding.etRemarks.text = null + binding.etUrl.text = null + binding.chkEnable.isChecked = true + return true + } + + /** + * 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() + } + + subItem.remarks = binding.etRemarks.text.toString() + subItem.url = binding.etUrl.text.toString() + subItem.enabled = binding.chkEnable.isChecked + + 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 +// } + + subStorage?.encode(subId, Gson().toJson(subItem)) + toast(R.string.toast_success) + finish() + return true + } + + /** + * save server config + */ + private fun deleteServer(): Boolean { + if (editSubId.isNotEmpty()) { + AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + MmkvManager.removeSubscription(editSubId) + finish() + } + .show() + } + return true + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.action_server, menu) + del_config = menu.findItem(R.id.del_config) + save_config = menu.findItem(R.id.save_config) + + if (editSubId.isEmpty()) { + 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/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt new file mode 100644 index 00000000..853a3878 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt @@ -0,0 +1,55 @@ +package com.v2ray.ang.ui + +import android.content.Intent +import androidx.recyclerview.widget.LinearLayoutManager +import android.view.Menu +import android.view.MenuItem +import com.v2ray.ang.R +import android.os.Bundle +import com.v2ray.ang.databinding.ActivitySubSettingBinding +import com.v2ray.ang.dto.SubscriptionItem +import com.v2ray.ang.util.MmkvManager + +class SubSettingActivity : BaseActivity() { + private lateinit var binding: ActivitySubSettingBinding + + var subscriptions:List> = listOf() + private val adapter by lazy { SubSettingRecyclerAdapter(this) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySubSettingBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + + title = getString(R.string.title_sub_setting) + + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = adapter + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + override fun onResume() { + super.onResume() + subscriptions = MmkvManager.decodeSubscriptions() + adapter.notifyDataSetChanged() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.action_sub_setting, menu) + menu.findItem(R.id.del_config)?.isVisible = false + menu.findItem(R.id.save_config)?.isVisible = false + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.add_config -> { + startActivity(Intent(this, SubEditActivity::class.java)) + true + } + else -> super.onOptionsItemSelected(item) + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt new file mode 100644 index 00000000..f3732bf3 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt @@ -0,0 +1,50 @@ +package com.v2ray.ang.ui + +import android.content.Intent +import android.graphics.Color +import android.view.LayoutInflater +import androidx.recyclerview.widget.RecyclerView +import android.view.ViewGroup +import com.google.gson.Gson +import com.tencent.mmkv.MMKV +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding +import com.v2ray.ang.util.MmkvManager + +class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter() { + + private var mActivity: SubSettingActivity = activity + private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) } + + override fun getItemCount() = mActivity.subscriptions.size + + override fun onBindViewHolder(holder: MainViewHolder, position: Int) { + val subId = mActivity.subscriptions[position].first + 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.colorSelected) + } else { + holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorUnselected) + } + holder.itemView.setBackgroundColor(Color.TRANSPARENT) + + holder.itemSubSettingBinding.layoutEdit.setOnClickListener { + mActivity.startActivity(Intent(mActivity, SubEditActivity::class.java) + .putExtra("subId", subId) + ) + } + holder.itemSubSettingBinding.infoContainer.setOnClickListener { + subItem.enabled = !subItem.enabled + subStorage?.encode(subId, Gson().toJson(subItem)) + notifyItemChanged(position) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder { + return MainViewHolder(ItemRecyclerSubSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + } + + class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) : RecyclerView.ViewHolder(itemSubSettingBinding.root) +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt similarity index 79% rename from V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt index 8600a4ee..da7ca188 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt @@ -1,46 +1,51 @@ package com.v2ray.ang.ui -import android.content.Intent +import android.app.Activity 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.ArrayAdapter import android.widget.ListView -import com.v2ray.ang.AppConfig +import java.util.ArrayList import com.v2ray.ang.R +import android.content.Intent +import android.text.TextUtils +import android.view.Menu +import android.view.MenuItem +import com.google.zxing.WriterException +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig import com.v2ray.ang.databinding.ActivityTaskerBinding -import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.util.MmkvManager class TaskerActivity : BaseActivity() { - private val binding by lazy { ActivityTaskerBinding.inflate(layoutInflater) } + private lateinit var binding: ActivityTaskerBinding private var listview: ListView? = null 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) + binding = ActivityTaskerBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) //add def value lstData.add("Default") lstGuid.add(AppConfig.TASKER_DEFAULT_GUID) - MmkvManager.decodeServerList().forEach { key -> + serverStorage?.allKeys()?.forEach { key -> MmkvManager.decodeServerConfig(key)?.let { config -> lstData.add(config.remarks) lstGuid.add(key) } } - val adapter = ArrayAdapter( - this, - android.R.layout.simple_list_item_single_choice, lstData - ) + val adapter = ArrayAdapter(this, + android.R.layout.simple_list_item_single_choice, lstData) listview = findViewById(R.id.listview) as ListView - listview?.adapter = adapter + listview!!.adapter = adapter init() } @@ -60,8 +65,8 @@ class TaskerActivity : BaseActivity() { listview?.setItemChecked(pos, true) } } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to initialize Tasker settings", e) + } catch (e: WriterException) { + e.printStackTrace() } } @@ -86,7 +91,7 @@ class TaskerActivity : BaseActivity() { intent.putExtra(AppConfig.TASKER_EXTRA_BUNDLE, extraBundle) intent.putExtra(AppConfig.TASKER_EXTRA_STRING_BLURB, blurb) - setResult(RESULT_OK, intent) + setResult(Activity.RESULT_OK, intent) finish() } @@ -101,12 +106,10 @@ class TaskerActivity : BaseActivity() { R.id.del_config -> { true } - R.id.save_config -> { confirmFinish() true } - else -> super.onOptionsItemSelected(item) } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UrlSchemeActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UrlSchemeActivity.kt new file mode 100644 index 00000000..90681100 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UrlSchemeActivity.kt @@ -0,0 +1,51 @@ +package com.v2ray.ang.ui + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import com.google.zxing.WriterException +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivityLogcatBinding +import com.v2ray.ang.extension.toast +import com.v2ray.ang.util.AngConfigManager + +class UrlSchemeActivity : BaseActivity() { + private lateinit var binding: ActivityLogcatBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityLogcatBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + + var shareUrl: String = "" + try { + intent?.apply { + when (action) { + Intent.ACTION_SEND -> { + if ("text/plain" == type) { + intent.getStringExtra(Intent.EXTRA_TEXT)?.let { + shareUrl = it + } + } + } + Intent.ACTION_VIEW -> { + val uri: Uri? = intent.data + shareUrl = uri?.getQueryParameter("url")!! + } + } + } + toast(shareUrl) + val count = AngConfigManager.importBatchConfig(shareUrl, "", false) + if (count > 0) { + toast(R.string.toast_success) + } else { + toast(R.string.toast_failure) + } + startActivity(Intent(this, MainActivity::class.java)) + finish() + } catch (e: WriterException) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetActivity.kt new file mode 100644 index 00000000..17755bfb --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetActivity.kt @@ -0,0 +1,216 @@ +package com.v2ray.ang.ui + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.OpenableColumns +import android.util.Log +import android.view.* +import android.view.View.GONE +import android.view.View.VISIBLE +import androidx.activity.result.contract.ActivityResultContracts +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.tbruyelle.rxpermissions.RxPermissions +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivitySubSettingBinding +import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding +import com.v2ray.ang.extension.toTrafficString +import com.v2ray.ang.extension.toast +import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File +import java.io.FileOutputStream +import java.net.HttpURLConnection +import java.net.InetSocketAddress +import java.net.Proxy +import java.net.URL +import java.text.DateFormat +import java.util.* + +class UserAssetActivity : BaseActivity() { + private lateinit var binding: ActivitySubSettingBinding + private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } + + val extDir by lazy { File(Utils.userAssetPath(this)) } + val geofiles = arrayOf("geosite.dat", "geoip.dat") + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySubSettingBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + title = getString(R.string.title_user_asset_setting) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = UserAssetAdapter() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_asset, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.add_file -> { + showFileChooser() + true + } + + R.id.download_file -> { + downloadGeoFiles() + true + } + + else -> super.onOptionsItemSelected(item) + } + + private fun showFileChooser() { + RxPermissions(this).request(Manifest.permission.READ_EXTERNAL_STORAGE).subscribe { + if (it) { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "*/*" + intent.addCategory(Intent.CATEGORY_OPENABLE) + + try { + chooseFile.launch( + Intent.createChooser( + intent, + getString(R.string.title_file_chooser) + ) + ) + } catch (ex: android.content.ActivityNotFoundException) { + toast(R.string.toast_require_file_manager) + } + } + } + } + + private val chooseFile = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + val uri = it.data?.data + if (it.resultCode == RESULT_OK && uri != null) { + try { + copyFile(uri) + } catch (e: Exception) { + toast(R.string.toast_asset_copy_failed) + } + } + } + + private fun copyFile(uri: Uri): String { + val targetFile = File(extDir, getCursorName(uri) ?: uri.toString()) + contentResolver.openInputStream(uri).use { inputStream -> + targetFile.outputStream().use { fileOut -> + inputStream?.copyTo(fileOut) + toast(R.string.toast_success) + binding.recyclerView.adapter?.notifyDataSetChanged() + } + } + return targetFile.path + } + + private fun getCursorName(uri: Uri): String? = try { + contentResolver.query(uri, null, null, null, null)?.let { cursor -> + cursor.run { + if (moveToFirst()) getString(getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) + else null + }.also { cursor.close() } + } + } catch (e: Exception) { + e.printStackTrace() + null + } + + private fun downloadGeoFiles() { + val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt()) + + toast(R.string.msg_downloading_content) + geofiles.forEach { + //toast(getString(R.string.msg_downloading_content) + it) + lifecycleScope.launch(Dispatchers.IO) { + val result = downloadGeo(it, 60000, httpPort) + launch(Dispatchers.Main) { + if (result) { + toast(getString(R.string.toast_success) + " " + it) + binding.recyclerView.adapter?.notifyDataSetChanged() + } else { + toast(getString(R.string.toast_failure) + " " + it) + } + } + } + } + } + + private fun downloadGeo(name: String, timeout: Int, httpPort: Int): Boolean { + val url = AppConfig.geoUrl + name + val targetTemp = File(extDir, name + "_temp") + val target = File(extDir, name) + var conn: HttpURLConnection? = null + //Log.d(AppConfig.ANG_PACKAGE, url) + + try { + conn = URL(url).openConnection( + Proxy( + Proxy.Type.HTTP, + InetSocketAddress("127.0.0.1", httpPort) + ) + ) as HttpURLConnection + conn.connectTimeout = timeout + conn.readTimeout = timeout + val inputStream = conn.inputStream + val responseCode = conn.responseCode + if (responseCode == HttpURLConnection.HTTP_OK) { + FileOutputStream(targetTemp).use { output -> + inputStream.copyTo(output) + } + + targetTemp.renameTo(target) + } + return true + } catch (e: Exception) { + Log.e(AppConfig.ANG_PACKAGE, Log.getStackTraceString(e)) + return false + } finally { + conn?.disconnect() + } + } + + inner class UserAssetAdapter : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder { + return UserAssetViewHolder(ItemRecyclerUserAssetBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + } + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) { + val file = extDir.listFiles()?.getOrNull(position) ?: return + holder.itemUserAssetBinding.assetName.text = file.name + val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM) + holder.itemUserAssetBinding.assetProperties.text = "${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}" + if (file.name in geofiles) { + holder.itemUserAssetBinding.layoutRemove.visibility = GONE + } else { + holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE + } + holder.itemUserAssetBinding.layoutRemove.setOnClickListener { + file.delete() + binding.recyclerView.adapter?.notifyItemRemoved(position) + } + } + + override fun getItemCount(): Int { + return extDir.listFiles()?.size ?: 0 + } + } + + class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) : RecyclerView.ViewHolder(itemUserAssetBinding.root) +} 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 new file mode 100644 index 00000000..5a58a8c4 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt @@ -0,0 +1,750 @@ +package com.v2ray.ang.util + +import android.content.Context +import android.content.SharedPreferences +import android.graphics.Bitmap +import android.text.TextUtils +import android.util.Log +import androidx.preference.PreferenceManager +import com.google.gson.Gson +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.ANG_CONFIG +import com.v2ray.ang.AppConfig.HTTPS_PROTOCOL +import com.v2ray.ang.AppConfig.HTTP_PROTOCOL +import com.v2ray.ang.R +import com.v2ray.ang.dto.* +import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_SECURITY +import com.v2ray.ang.dto.V2rayConfig.Companion.TLS +import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER +import java.net.URI +import java.util.* +import com.v2ray.ang.extension.idnHost + +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) + 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) + } + } + 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)) + } + } + + /** + * import config form qrcode or... + */ + private fun importConfig(str: String?, subid: String, removedSelectedServer: ServerConfig?): Int { + try { + if (str == null || TextUtils.isEmpty(str)) { + return R.string.toast_none_data + } + + //maybe sub + if (TextUtils.isEmpty(subid) && (str.startsWith(HTTP_PROTOCOL) || str.startsWith(HTTPS_PROTOCOL))) { + MmkvManager.importUrlAsSubscription(str) + return 0 + } + + var config: ServerConfig? = null + val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false + if (str.startsWith(EConfigType.VMESS.protocolScheme)) { + config = ServerConfig.create(EConfigType.VMESS) + val streamSetting = config.outboundBean?.streamSettings ?: return -1 + + var fingerprint = streamSetting.tlsSettings?.fingerprint + + + if (!tryParseNewVmess(str, config, allowInsecure)) { + if (str.indexOf("?") > 0) { + if (!tryResolveVmess4Kitsunebi(str, config)) { + return R.string.toast_incorrect_protocol + } + } else { + var result = str.replace(EConfigType.VMESS.protocolScheme, "") + result = Utils.decode(result) + if (TextUtils.isEmpty(result)) { + return R.string.toast_decoding_failed + } + 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) + ) { + return R.string.toast_incorrect_protocol + } + + 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)) 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) + + + streamSetting.populateTlsSettings(vmessQRCode.tls, allowInsecure, + if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni, fingerprint, vmessQRCode.alpn) + } + } + } else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) { + 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 R.string.toast_incorrect_protocol + + 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() + } + } + } else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) { + var result = str.replace(EConfigType.SOCKS.protocolScheme, "") + val indexSplit = result.indexOf("#") + config = ServerConfig.create(EConfigType.SOCKS) + 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 R.string.toast_incorrect_protocol + + 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].lowercase() + socksUsersBean.pass = match.groupValues[2] + server.users = listOf(socksUsersBean) + } + } else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) { + val uri = URI(Utils.fixIllegalUrl(str)) + config = ServerConfig.create(EConfigType.TROJAN) + config.remarks = Utils.urlDecode(uri.fragment ?: "") + + var flow = "" + var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint + if (uri.rawQuery != null) { + 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"]) + config.outboundBean?.streamSettings?.populateTlsSettings(queryParam["security"] ?: TLS, allowInsecure, queryParam["sni"] ?: sni!!, fingerprint, queryParam["alpn"]) + flow = queryParam["flow"] ?: "" + } else { + + config.outboundBean?.streamSettings?.populateTlsSettings(TLS, allowInsecure, "", fingerprint, null) + } + + config.outboundBean?.settings?.servers?.get(0)?.let { server -> + server.address = uri.idnHost + server.port = uri.port + server.password = uri.userInfo + server.flow = flow + } + } else if (str.startsWith(EConfigType.VLESS.protocolScheme)) { + val uri = URI(Utils.fixIllegalUrl(str)) + val queryParam = uri.rawQuery.split("&") + .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } + config = ServerConfig.create(EConfigType.VLESS) + val streamSetting = config.outboundBean?.streamSettings ?: return -1 + var fingerprint = streamSetting.tlsSettings?.fingerprint + + config.remarks = Utils.urlDecode(uri.fragment ?: "") + 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"] ?: "" + } + + val sni = streamSetting.populateTransportSettings(queryParam["type"] ?: "tcp", queryParam["headerType"], + queryParam["host"], queryParam["path"], queryParam["seed"], queryParam["quicSecurity"], queryParam["key"], + queryParam["mode"], queryParam["serviceName"]) + streamSetting.populateTlsSettings(queryParam["security"] ?: "", allowInsecure, queryParam["sni"] ?: sni, fingerprint, queryParam["alpn"]) + } + 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 + } + + private fun tryParseNewVmess(uriString: String, config: ServerConfig, allowInsecure: Boolean): Boolean { + return runCatching { + val uri = URI(uriString) + check(uri.scheme == "vmess") + val (_, protocol, tlsStr, uuid, alterId) = + Regex("(tcp|http|ws|kcp|quic|grpc)(\\+tls)?:([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})") + .matchEntire(uri.userInfo)?.groupValues + ?: error("parse user info fail.") + val tls = tlsStr.isNotBlank() + val queryParam = uri.rawQuery.split("&") + .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } + + val streamSetting = config.outboundBean?.streamSettings ?: return false + config.remarks = Utils.urlDecode(uri.fragment ?: "") + config.outboundBean.settings?.vnext?.get(0)?.let { vnext -> + vnext.address = uri.idnHost + vnext.port = uri.port + vnext.users[0].id = uuid + vnext.users[0].security = DEFAULT_SECURITY + vnext.users[0].alterId = alterId.toInt() + } + var fingerprint = streamSetting.tlsSettings?.fingerprint + val sni = streamSetting.populateTransportSettings(protocol, queryParam["type"], + queryParam["host"]?.split("|")?.get(0) ?: "", + queryParam["path"]?.takeIf { it.trim() != "/" } ?: "", queryParam["seed"], queryParam["security"], + queryParam["key"], queryParam["mode"], queryParam["serviceName"]) + streamSetting.populateTlsSettings(if (tls) TLS else "", allowInsecure, sni, fingerprint, null) + true + }.getOrElse { false } + } + + private fun tryResolveVmess4Kitsunebi(server: String, config: ServerConfig): Boolean { + + var result = server.replace(EConfigType.VMESS.protocolScheme, "") + val indexSplit = result.indexOf("?") + if (indexSplit > 0) { + result = result.substring(0, indexSplit) + } + result = Utils.decode(result) + + val arr1 = result.split('@') + if (arr1.count() != 2) { + return false + } + val arr21 = arr1[0].split(':') + val arr22 = arr1[1].split(':') + if (arr21.count() != 2) { + return false + } + + config.remarks = "Alien" + config.outboundBean?.settings?.vnext?.get(0)?.let { vnext -> + vnext.address = arr22[0] + vnext.port = Utils.parseInt(arr22[1]) + vnext.users[0].id = arr21[1] + vnext.users[0].security = arr21[0] + vnext.users[0].alterId = 0 + } + return true + } + + private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean { + try { + val uri = URI(Utils.fixIllegalUrl(str)) + config.remarks = Utils.urlDecode(uri.fragment ?: "") + + 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(":") + } + + 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 + } + } + + /** + * share config + */ + private fun shareConfig(guid: String): String { + try { + val config = MmkvManager.decodeServerConfig(guid) ?: return "" + val outbound = config.getProxyOutbound() ?: return "" + val streamSetting = outbound.streamSettings ?: return "" + return config.configType.protocolScheme + when (config.configType) { + EConfigType.VMESS -> { + 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() + outbound.getTransportSettingDetails()?.let { transportDetails -> + vmessQRCode.type = transportDetails[0] + vmessQRCode.host = transportDetails[1] + vmessQRCode.path = transportDetails[2] + } + val json = Gson().toJson(vmessQRCode) + Utils.encode(json) + } + EConfigType.CUSTOM, EConfigType.WIREGUARD -> "" + EConfigType.SHADOWSOCKS -> { + 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()) + url + remark + } + EConfigType.SOCKS -> { + val remark = "#" + Utils.urlEncode(config.remarks) + val pw = Utils.encode("${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}") + val url = String.format("%s@%s:%s", + pw, + Utils.getIpv6Address(outbound.getServerAddress()!!), + outbound.getServerPort()) + url + remark + } + EConfigType.VLESS, + EConfigType.TROJAN -> { + val remark = "#" + Utils.urlEncode(config.remarks) + + val dicQuery = HashMap() + if (config.configType == EConfigType.VLESS) { + 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() + } else if (config.configType == EConfigType.TROJAN) { + config.outboundBean?.settings?.servers?.get(0)?.flow?.let { + if (!TextUtils.isEmpty(it)) { + dicQuery["flow"] = it + } + } + } + + dicQuery["security"] = streamSetting.security.ifEmpty { "none" } + (streamSetting.tlsSettings?: streamSetting.xtlsSettings)?.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() + } + } + 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" -> { + 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["serviceName"] = 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()) + url + query + remark + } + } + } 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 Utils.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(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) ?: "")?.let { + if (it.subscriptionId == subid) { + return@let it + } + return@let null + } + } else { + null + } + if(!append) { + MmkvManager.removeServerViaSubid(subid) + } +// var servers = server +// if (server.indexOf("vmess") >= 0 && server.indexOf("vmess") == server.lastIndexOf("vmess")) { +// servers = server.replace("\n", "") +// } + + var count = 0 + servers.lines() + .forEach { + val resId = importConfig(it, subid, removedSelectedServer) + if (resId == 0) { + count++ + } + } + return count + } catch (e: Exception) { + e.printStackTrace() + } + return 0 + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt new file mode 100644 index 00000000..b95b408b --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt @@ -0,0 +1,43 @@ +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 rx.Observable +import java.util.* + +object AppManagerUtil { + 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 appName = applicationInfo.loadLabel(packageManager).toString() + val appIcon = applicationInfo.loadIcon(packageManager) + val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0 + + val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0) + apps.add(appInfo) + } + + return apps + } + + fun rxLoadNetworkAppList(ctx: Context): Observable> = Observable.unsafeCreate { + it.onNext(loadNetworkAppList(ctx)) + } + + 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/MessageUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MessageUtil.kt similarity index 58% rename from V2rayNG/app/src/main/java/com/v2ray/ang/util/MessageUtil.kt rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MessageUtil.kt index c84443c7..0f521f57 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/MessageUtil.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MessageUtil.kt @@ -3,43 +3,21 @@ package com.v2ray.ang.util import android.content.ComponentName import android.content.Context import android.content.Intent -import android.util.Log import com.v2ray.ang.AppConfig import com.v2ray.ang.service.V2RayTestService import java.io.Serializable + object MessageUtil { - - /** - * Sends a message to the service. - * - * @param ctx The context. - * @param what The message identifier. - * @param content The message content. - */ fun sendMsg2Service(ctx: Context, what: Int, content: Serializable) { sendMsg(ctx, AppConfig.BROADCAST_ACTION_SERVICE, what, content) } - /** - * Sends a message to the UI. - * - * @param ctx The context. - * @param what The message identifier. - * @param content The message content. - */ fun sendMsg2UI(ctx: Context, what: Int, content: Serializable) { sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content) } - /** - * Sends a message to the test service. - * - * @param ctx The context. - * @param what The message identifier. - * @param content The message content. - */ fun sendMsg2TestService(ctx: Context, what: Int, content: Serializable) { try { val intent = Intent() @@ -48,18 +26,10 @@ object MessageUtil { intent.putExtra("content", content) ctx.startService(intent) } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to send message to test service", e) + e.printStackTrace() } } - /** - * Sends a message with the specified action. - * - * @param ctx The context. - * @param action The action string. - * @param what The message identifier. - * @param content The message content. - */ private fun sendMsg(ctx: Context, action: String, what: Int, content: Serializable) { try { val intent = Intent() @@ -69,7 +39,7 @@ object MessageUtil { intent.putExtra("content", content) ctx.sendBroadcast(intent) } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to send message with action: $action", e) + e.printStackTrace() } } } 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 new file mode 100644 index 00000000..533527e7 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt @@ -0,0 +1,179 @@ +package com.v2ray.ang.util + +import com.google.gson.Gson +import com.tencent.mmkv.MMKV +import com.v2ray.ang.dto.ServerAffiliationInfo +import com.v2ray.ang.dto.ServerConfig +import com.v2ray.ang.dto.SubscriptionItem + +object MmkvManager { + const val ID_MAIN = "MAIN" + const val ID_SERVER_CONFIG = "SERVER_CONFIG" + const val ID_SERVER_RAW = "SERVER_RAW" + const val ID_SERVER_AFF = "SERVER_AFF" + const val ID_SUB = "SUB" + const val ID_SETTING = "SETTING" + const val KEY_SELECTED_SERVER = "SELECTED_SERVER" + const val KEY_ANG_CONFIGS = "ANG_CONFIGS" + + private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) } + private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, 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) } + + 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 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(key) + mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList)) + if (mainStorage?.decodeString(KEY_SELECTED_SERVER).isNullOrBlank()) { + mainStorage?.encode(KEY_SELECTED_SERVER, key) + } + } + 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) + 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() { + serverAffStorage?.allKeys()?.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 subItem = SubscriptionItem() + subItem.remarks = "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))) + } + } + subscriptions.sortedBy { (_, value) -> value.addedTime } + return subscriptions + } + + fun removeSubscription(subid: String) { + subStorage?.remove(subid) + removeServerViaSubid(subid) + } + + fun removeAllServer() { + mainStorage?.clearAll() + serverStorage?.clearAll() + serverAffStorage?.clearAll() + } + + fun removeInvalidServer() { + 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/java/com/v2ray/ang/util/MyContextWrapper.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MyContextWrapper.kt similarity index 81% rename from V2rayNG/app/src/main/java/com/v2ray/ang/util/MyContextWrapper.kt rename to V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MyContextWrapper.kt index a769368f..cf9bd35e 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/MyContextWrapper.kt +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MyContextWrapper.kt @@ -7,22 +7,16 @@ import android.content.res.Resources import android.os.Build import android.os.LocaleList import androidx.annotation.RequiresApi -import java.util.Locale +import java.util.* open class MyContextWrapper(base: Context?) : ContextWrapper(base) { companion object { - /** - * Wraps the context with a new locale. - * - * @param context The original context. - * @param newLocale The new locale to set. - * @return A ContextWrapper with the new locale. - */ @RequiresApi(Build.VERSION_CODES.N) fun wrap(context: Context, newLocale: Locale?): ContextWrapper { var mContext = context val res: Resources = mContext.resources val configuration: Configuration = res.configuration + //注意 Android 7.0 前后的不同处理方法 mContext = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { configuration.setLocale(newLocale) val localeList = LocaleList(newLocale) diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/QRCodeDecoder.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/QRCodeDecoder.kt new file mode 100644 index 00000000..250f84df --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/QRCodeDecoder.kt @@ -0,0 +1,100 @@ +package com.v2ray.ang.util + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import com.google.zxing.* +import com.google.zxing.common.GlobalHistogramBinarizer +import com.google.zxing.common.HybridBinarizer +import java.util.* + +/** + * 描述:解析二维码图片 + */ +object QRCodeDecoder { + val HINTS: MutableMap = EnumMap(DecodeHintType::class.java) + + /** + * 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。 + * + * @param picturePath 要解析的二维码图片本地路径 + * @return 返回二维码图片里的内容 或 null + */ + fun syncDecodeQRCode(picturePath: String): String? { + return syncDecodeQRCode(getDecodeAbleBitmap(picturePath)) + } + + /** + * 同步解析bitmap二维码。该方法是耗时操作,请在子线程中调用。 + * + * @param bitmap 要解析的二维码图片 + * @return 返回二维码图片里的内容 或 null + */ + fun syncDecodeQRCode(bitmap: Bitmap?): String? { + 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) + return MultiFormatReader().decode(BinaryBitmap(HybridBinarizer(source)), HINTS).text + } catch (e: Exception) { + e.printStackTrace() + } + if (source != null) { + try { + return MultiFormatReader().decode(BinaryBitmap(GlobalHistogramBinarizer(source)), HINTS).text + } catch (e2: Throwable) { + e2.printStackTrace() + } + } + return null + } + + /** + * 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR + * + * @param picturePath 本地图片文件路径 + * @return + */ + private fun getDecodeAbleBitmap(picturePath: String): Bitmap? { + return try { + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(picturePath, options) + var sampleSize = options.outHeight / 400 + if (sampleSize <= 0) { + sampleSize = 1 + } + options.inSampleSize = sampleSize + options.inJustDecodeBounds = false + BitmapFactory.decodeFile(picturePath, options) + } catch (e: Exception) { + null + } + } + + init { + val allFormats: List = arrayListOf( + BarcodeFormat.AZTEC + ,BarcodeFormat.CODABAR + ,BarcodeFormat.CODE_39 + ,BarcodeFormat.CODE_93 + ,BarcodeFormat.CODE_128 + ,BarcodeFormat.DATA_MATRIX + ,BarcodeFormat.EAN_8 + ,BarcodeFormat.EAN_13 + ,BarcodeFormat.ITF + ,BarcodeFormat.MAXICODE + ,BarcodeFormat.PDF_417 + ,BarcodeFormat.QR_CODE + ,BarcodeFormat.RSS_14 + ,BarcodeFormat.RSS_EXPANDED + ,BarcodeFormat.UPC_A + ,BarcodeFormat.UPC_E + ,BarcodeFormat.UPC_EAN_EXTENSION) + HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE + HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats + HINTS[DecodeHintType.CHARACTER_SET] = "utf-8" + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/SpeedtestUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/SpeedtestUtil.kt new file mode 100644 index 00000000..5a0e5989 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/SpeedtestUtil.kt @@ -0,0 +1,142 @@ +package com.v2ray.ang.util + +import android.content.Context +import android.os.SystemClock +import android.text.TextUtils +import android.util.Log +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.extension.responseLength +import kotlinx.coroutines.isActive +import libv2ray.Libv2ray +import java.io.IOException +import java.net.* +import java.util.* +import kotlin.coroutines.coroutineContext + +object SpeedtestUtil { + + private val tcpTestingSockets = ArrayList() + + suspend fun tcping(url: String, port: Int): Long { + var time = -1L + for (k in 0 until 2) { + val one = socketConnectTime(url, port) + if (!coroutineContext.isActive) { + break + } + if (one != -1L && (time == -1L || one < time)) { + time = one + } + } + return time + } + + fun realPing(config: String): Long { + return try { + Libv2ray.measureOutboundDelay(config) + } catch (e: Exception) { + Log.d(AppConfig.ANG_PACKAGE, "realPing: $e") + -1L + } + } + + fun ping(url: String): String { + try { + val command = "/system/bin/ping -c 3 $url" + val process = Runtime.getRuntime().exec(command) + val allText = process.inputStream.bufferedReader().use { it.readText() } + if (!TextUtils.isEmpty(allText)) { + val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19) + val temps = tempInfo.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + if (temps.count() > 0 && temps[0].length < 10) { + return temps[0].toFloat().toInt().toString() + "ms" + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return "-1ms" + } + + fun socketConnectTime(url: String, port: Int): Long { + try { + val socket = Socket() + synchronized(this) { + tcpTestingSockets.add(socket) + } + val start = System.currentTimeMillis() + socket.connect(InetSocketAddress(url, port),3000) + val time = System.currentTimeMillis() - start + synchronized(this) { + tcpTestingSockets.remove(socket) + } + socket.close() + return time + } catch (e: UnknownHostException) { + e.printStackTrace() + } catch (e: IOException) { + Log.d(AppConfig.ANG_PACKAGE, "socketConnectTime IOException: $e") + } catch (e: Exception) { + e.printStackTrace() + } + return -1 + } + + fun closeAllTcpSockets() { + synchronized(this) { + tcpTestingSockets.forEach { + it?.close() + } + tcpTestingSockets.clear() + } + } + + fun testConnection(context: Context, port: Int): String { + // return V2RayVpnService.measureV2rayDelay() + var result: String + var conn: HttpURLConnection? = null + + try { + val url = URL("https", + "www.google.com", + "/generate_204") + + conn = url.openConnection( + Proxy(Proxy.Type.HTTP, + InetSocketAddress("127.0.0.1", port))) as HttpURLConnection + conn.connectTimeout = 30000 + conn.readTimeout = 30000 + conn.setRequestProperty("Connection", "close") + conn.instanceFollowRedirects = false + conn.useCaches = false + + val start = SystemClock.elapsedRealtime() + val code = conn.responseCode + val elapsed = SystemClock.elapsedRealtime() - start + + if (code == 204 || code == 200 && conn.responseLength == 0L) { + result = context.getString(R.string.connection_test_available, elapsed) + } else { + throw IOException(context.getString(R.string.connection_test_error_status_code, code)) + } + } catch (e: IOException) { + // network exception + 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 + Log.d(AppConfig.ANG_PACKAGE, "testConnection Exception: " + Log.getStackTraceString(e)) + result = context.getString(R.string.connection_test_error, e.message) + } finally { + conn?.disconnect() + } + + return result + } + + fun getLibVersion(): String { + return Libv2ray.checkVersionX() + } + +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt new file mode 100644 index 00000000..be6d5edb --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt @@ -0,0 +1,433 @@ +package com.v2ray.ang.util + +import android.content.ClipboardManager +import android.content.Context +import android.text.Editable +import android.util.Base64 +import com.google.zxing.WriterException +import android.graphics.Bitmap +import com.google.zxing.BarcodeFormat +import com.google.zxing.qrcode.QRCodeWriter +import com.google.zxing.EncodeHintType +import java.util.* +import kotlin.collections.HashMap +import android.content.ClipData +import android.content.Intent +import android.content.res.Configuration.UI_MODE_NIGHT_MASK +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.net.Uri +import android.os.Build +import android.os.LocaleList +import android.util.Log +import android.util.Patterns +import android.webkit.URLUtil +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.BuildConfig +import com.v2ray.ang.R +import com.v2ray.ang.extension.toast +import java.net.* +import com.v2ray.ang.service.V2RayServiceManager +import java.io.IOException + +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 + * + * @param text + * @return + */ + fun getEditable(text: String): Editable { + return Editable.Factory.getInstance().newEditable(text) + } + + /** + * find value in array position + */ + fun arrayFind(array: Array, value: String): Int { + for (i in array.indices) { + if (array[i] == value) { + return i + } + } + return -1 + } + + /** + * parseInt + */ + fun parseInt(str: String): Int { + return parseInt(str, 0) + } + + fun parseInt(str: String?, default: Int): Int { + str ?: return default + return try { + Integer.parseInt(str) + } catch (e: Exception) { + e.printStackTrace() + default + } + } + + /** + * get text from clipboard + */ + fun getClipboard(context: Context): String { + return try { + val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cmb.primaryClip?.getItemAt(0)?.text.toString() + } catch (e: Exception) { + e.printStackTrace() + "" + } + } + + /** + * set text to clipboard + */ + fun setClipboard(context: Context, content: String) { + try { + val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText(null, content) + cmb.setPrimaryClip(clipData) + } catch (e: Exception) { + e.printStackTrace() + } + } + + /** + * base64 decode + */ + fun decode(text: String): String { + tryDecodeBase64(text)?.let { return it } + if (text.endsWith('=')) { + // try again for some loosely formatted base64 + tryDecodeBase64(text.trimEnd('='))?.let { return it } + } + return "" + } + + fun tryDecodeBase64(text: String): String? { + try { + return Base64.decode(text, Base64.NO_WRAP).toString(charset("UTF-8")) + } catch (e: Exception) { + Log.i(ANG_PACKAGE, "Parse base64 standard failed $e") + } + try { + return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(charset("UTF-8")) + } catch (e: Exception) { + Log.i(ANG_PACKAGE, "Parse base64 url safe failed $e") + } + return null + } + + /** + * base64 encode + */ + fun encode(text: String): String { + return try { + Base64.encodeToString(text.toByteArray(charset("UTF-8")), Base64.NO_WRAP) + } catch (e: Exception) { + e.printStackTrace() + "" + } + } + + /** + * get remote dns servers from preference + */ + fun getRemoteDnsServers(): List { + val remoteDns = settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_AGENT + val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) } + if (ret.isEmpty()) { + return listOf(AppConfig.DNS_AGENT) + } + return ret + } + + fun getVpnDnsServers(): List { + val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS) + ?: settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS) + ?: AppConfig.DNS_AGENT + return vpnDns.split(",").filter { isPureIpAddress(it) } + // allow empty, in that case dns will use system default + } + + /** + * get remote dns servers from preference + */ + fun getDomesticDnsServers(): List { + val domesticDns = settingsStorage?.decodeString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT + val ret = domesticDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) } + if (ret.isEmpty()) { + return listOf(AppConfig.DNS_DIRECT) + } + return ret + } + + /** + * 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() + } + + } + } + val bitmap = Bitmap.createBitmap(size, size, + Bitmap.Config.ARGB_8888) + bitmap.setPixels(pixels, 0, size, 0, 0, size, size) + return bitmap + } catch (e: WriterException) { + e.printStackTrace() + return null + } + } + + /** + * is ip address + */ + fun isIpAddress(value: String): Boolean { + try { + var addr = value + if (addr.isEmpty() || addr.isBlank()) { + return false + } + //CIDR + if (addr.indexOf("/") > 0) { + val arr = addr.split("/") + if (arr.count() == 2 && Integer.parseInt(arr[1]) > 0) { + addr = arr[0] + } + } + + // "::ffff:192.168.173.22" + // "[::ffff:192.168.173.22]:80" + if (addr.startsWith("::ffff:") && '.' in addr) { + addr = addr.drop(7) + } else if (addr.startsWith("[::ffff:") && '.' in addr) { + addr = addr.drop(8).replace("]", "") + } + + // addr = addr.toLowerCase() + val octets = addr.split('.').toTypedArray() + if (octets.size == 4) { + if (octets[3].indexOf(":") > 0) { + addr = addr.substring(0, addr.indexOf(":")) + } + return isIpv4Address(addr) + } + + // Ipv6addr [2001:abc::123]:8080 + return isIpv6Address(addr) + } catch (e: Exception) { + e.printStackTrace() + return false + } + } + + fun isPureIpAddress(value: String): Boolean { + return (isIpv4Address(value) || isIpv6Address(value)) + } + + fun isIpv4Address(value: String): Boolean { + val regV4 = Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$") + return regV4.matches(value) + } + + fun isIpv6Address(value: String): Boolean { + var addr = value + if (addr.indexOf("[") == 0 && addr.lastIndexOf("]") > 0) { + addr = addr.drop(1) + addr = addr.dropLast(addr.count() - addr.lastIndexOf("]")) + } + val regV6 = Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$") + return regV6.matches(addr) + } + + private fun isCoreDNSAddress(s: String): Boolean { + return s.startsWith("https") || s.startsWith("tcp") || s.startsWith("quic") + } + + /** + * is valid url + */ + fun isValidUrl(value: String?): Boolean { + try { + if (value != null && Patterns.WEB_URL.matcher(value).matches() || URLUtil.isValidUrl(value)) { + return true + } + } catch (e: WriterException) { + e.printStackTrace() + return false + } + return false + } + + fun startVServiceFromToggle(context: Context): Boolean { + if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) { + context.toast(R.string.app_tile_first_use) + return false + } + V2RayServiceManager.startV2Ray(context) + return true + } + + /** + * stopVService + */ + fun stopVService(context: Context) { + context.toast(R.string.toast_services_stop) + MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "") + } + + fun openUri(context: Context, uriString: String) { + val uri = Uri.parse(uriString) + context.startActivity(Intent(Intent.ACTION_VIEW, uri)) + } + + /** + * uuid + */ + fun getUuid(): String { + return try { + UUID.randomUUID().toString().replace("-", "") + } catch (e: Exception) { + e.printStackTrace() + "" + } + } + + fun urlDecode(url: String): String { + return try { + URLDecoder.decode(URLDecoder.decode(url), "utf-8") + } catch (e: Exception) { + e.printStackTrace() + url + } + } + + fun urlEncode(url: String): String { + return try { + URLEncoder.encode(url, "UTF-8") + } catch (e: Exception) { + e.printStackTrace() + url + } + } + + + /** + * readTextFromAssets + */ + fun readTextFromAssets(context: Context, fileName: String): String { + val content = context.assets.open(fileName).bufferedReader().use { + it.readText() + } + return content + } + + fun userAssetPath(context: Context?): String { + if (context == null) + return "" + val extDir = context.getExternalFilesDir(AppConfig.DIR_ASSETS) + ?: return context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath + return extDir.absolutePath + } + + fun getUrlContext(url: String, timeout: Int): String { + var result: String + var conn: HttpURLConnection? = null + + try { + conn = URL(url).openConnection() as HttpURLConnection + conn.connectTimeout = timeout + conn.readTimeout = timeout + conn.setRequestProperty("Connection", "close") + conn.instanceFollowRedirects = false + conn.useCaches = false + //val code = conn.responseCode + result = conn.inputStream.bufferedReader().readText() + } catch (e: Exception) { + result = "" + } finally { + conn?.disconnect() + } + return result + } + + @Throws(IOException::class) + fun getUrlContentWithCustomUserAgent(urlStr: String?): String { + val url = URL(urlStr) + val conn = url.openConnection() + conn.setRequestProperty("Connection", "close") + conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}") + url.userInfo?.let { + conn.setRequestProperty("Authorization", + "Basic ${encode(urlDecode(it))}") + } + conn.useCaches = false + return conn.inputStream.use { + it.bufferedReader().readText() + } + } + + fun getDarkModeStatus(context: Context): Boolean { + val mode = context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK + return mode == UI_MODE_NIGHT_YES + } + + fun getIpv6Address(address: String): String { + return if (isIpv6Address(address)) { + String.format("[%s]", address) + } else { + address + } + } + + fun getLocale(context: Context): Locale = + when (settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: "auto") { + "auto" -> getSysLocale() + "en" -> Locale("en") + "zh-rCN" -> Locale("zh", "CN") + "zh-rTW" -> Locale("zh", "TW") + "vi" -> Locale("vi") + "ru" -> Locale("ru") + "fa" -> Locale("fa") + else -> getSysLocale() + } + + private fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + LocaleList.getDefault()[0] + } else { + Locale.getDefault() + } + + fun fixIllegalUrl(str: String): String { + return str + .replace(" ","%20") + .replace("|","%7C") + } + + fun removeWhiteSpace(str: String?): String? { + return str?.replace(" ", "") + } +} + 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 new file mode 100644 index 00000000..f5177aca --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt @@ -0,0 +1,432 @@ +package com.v2ray.ang.util + +import android.content.Context +import android.text.TextUtils +import android.util.Log +import com.google.gson.* +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ERoutingMode +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 result = getV2rayNonCustomConfig(context, outbound) + 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): 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) + + httpRequestObject(outbound) + + v2rayConfig.outbounds[0] = outbound + + 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 + } + 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 + 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_FAKE_DNS_ENABLED) == true) { + v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean()) + v2rayConfig.outbounds.filter { it.protocol == "freedom" }.forEach { + it.settings?.domainStrategy = "UseIP" + } + } + } + + /** + * routing + */ + private fun routing(v2rayConfig: V2rayConfig): Boolean { + try { + routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT) + ?: "", AppConfig.TAG_AGENT, v2rayConfig) + routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT) + ?: "", AppConfig.TAG_DIRECT, v2rayConfig) + routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED) + ?: "", AppConfig.TAG_BLOCKED, v2rayConfig) + + v2rayConfig.routing.domainStrategy = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) + ?: "IPIfNonMatch" + v2rayConfig.routing.domainMatcher = "mph" + val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value + + // Hardcode googleapis.cn + val googleapisRoute = V2rayConfig.RoutingBean.RulesBean( + type = "field", + outboundTag = AppConfig.TAG_AGENT, + domain = arrayListOf("domain:googleapis.cn") + ) + + when (routingMode) { + ERoutingMode.BYPASS_LAN.value -> { + routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig) + } + ERoutingMode.BYPASS_MAINLAND.value -> { + routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig) + v2rayConfig.routing.rules.add(0, googleapisRoute) + } + ERoutingMode.BYPASS_LAN_MAINLAND.value -> { + routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig) + routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig) + v2rayConfig.routing.rules.add(0, googleapisRoute) + } + ERoutingMode.GLOBAL_DIRECT.value -> { + val globalDirect = V2rayConfig.RoutingBean.RulesBean( + type = "field", + outboundTag = AppConfig.TAG_DIRECT, + port = "0-65535" + ) + v2rayConfig.routing.rules.add(globalDirect) + } + } + } 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.type = "field" + 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.type = "field" + 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.type = "field" + rulesDomain.outboundTag = tag + rulesDomain.domain = ArrayList() + + //IP + val rulesIP = V2rayConfig.RoutingBean.RulesBean() + rulesIP.type = "field" + rulesIP.outboundTag = tag + rulesIP.ip = ArrayList() + + userRule.split(",").map { it.trim() }.forEach { + if (Utils.isIpAddress(it) || it.startsWith("geoip:")) { + rulesIP.ip?.add(it) + } else if (it.isNotEmpty()) +// if (Utils.isValidUrl(it) +// || it.startsWith("geosite:") +// || it.startsWith("regexp:") +// || it.startsWith("domain:") +// || it.startsWith("full:")) + { + rulesDomain.domain?.add(it) + } + } + if (rulesDomain.domain?.size!! > 0) { + v2rayConfig.routing.rules.add(rulesDomain) + } + if (rulesIP.ip?.size!! > 0) { + v2rayConfig.routing.rules.add(rulesIP) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun userRule2Domian(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 = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT) + ?: "") + val directDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_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 "1.1.1.1", + 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( + type = "field", + 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() + val remoteDns = Utils.getRemoteDnsServers() + val proxyDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT) + ?: "") + + remoteDns.forEach { + servers.add(it) + } + if (proxyDomain.size > 0) { + servers.add(V2rayConfig.DnsBean.ServersBean(remoteDns.first(), 53, proxyDomain, null)) + } + + // domestic DNS + val directDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT) + ?: "") + val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value + if (directDomain.size > 0 || routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) { + val domesticDns = Utils.getDomesticDnsServers() + val geositeCn = arrayListOf("geosite:cn") + val geoipCn = arrayListOf("geoip:cn") + if (directDomain.size > 0) { + servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, directDomain, geoipCn)) + } + if (routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) { + servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, geositeCn, geoipCn)) + } + if (Utils.isPureIpAddress(domesticDns.first())) { + v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean( + type = "field", + outboundTag = AppConfig.TAG_DIRECT, + port = "53", + ip = arrayListOf(domesticDns.first()), + domain = null) + ) + } + } + + val blkDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED) + ?: "") + 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" + + // 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( + type = "field", + outboundTag = AppConfig.TAG_AGENT, + port = "53", + ip = arrayListOf(remoteDns.first()), + domain = null) + ) + } + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + private fun httpRequestObject(outbound: V2rayConfig.OutboundBean): Boolean { + try { + 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 + } + +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt new file mode 100644 index 00000000..30673162 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt @@ -0,0 +1,235 @@ +package com.v2ray.ang.viewmodel + +import android.app.Application +import android.content.* +import android.util.Log +import android.view.LayoutInflater +import android.widget.ArrayAdapter +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.google.gson.Gson +import com.tencent.mmkv.MMKV +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.databinding.DialogConfigFilterBinding +import com.v2ray.ang.dto.* +import com.v2ray.ang.extension.toast +import com.v2ray.ang.util.* +import com.v2ray.ang.util.MmkvManager.KEY_ANG_CONFIGS +import kotlinx.coroutines.* +import java.util.* + +class MainViewModel(application: Application) : AndroidViewModel(application) { + 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) } + + var serverList = MmkvManager.decodeServerList() + var subscriptionId: String = "" + var keywordFilter: String = "" + private set + val serversCache = mutableListOf() + val isRunning by lazy { MutableLiveData() } + val updateListAction by lazy { MutableLiveData() } + val updateTestResultAction by lazy { MutableLiveData() } + + private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) } + + fun startListenBroadcast() { + isRunning.value = false + getApplication().registerReceiver(mMsgReceiver, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)) + MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "") + } + + override fun onCleared() { + getApplication().unregisterReceiver(mMsgReceiver) + tcpingTestScope.coroutineContext[Job]?.cancelChildren() + SpeedtestUtil.closeAllTcpSockets() + Log.i(ANG_PACKAGE, "Main ViewModel is cleared") + super.onCleared() + } + + fun reloadServerList() { + serverList = MmkvManager.decodeServerList() + updateCache() + updateListAction.value = -1 + } + + fun removeServer(guid: String) { + serverList.remove(guid) + MmkvManager.removeServer(guid) + val index = getPosition(guid) + if(index >= 0){ + serversCache.removeAt(index) + } + } + + fun appendCustomConfigServer(server: String) { + val config = ServerConfig.create(EConfigType.CUSTOM) + config.remarks = System.currentTimeMillis().toString() + config.subscriptionId = subscriptionId + config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java) + val key = MmkvManager.encodeServerConfig("", config) + serverRawStorage?.encode(key, server) + serverList.add(key) + serversCache.add(ServersCache(key,config)) + } + + fun swapServer(fromPosition: Int, toPosition: Int) { + Collections.swap(serverList, fromPosition, toPosition) + Collections.swap(serversCache, fromPosition, toPosition) + mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList)) + } + + @Synchronized + fun updateCache() { + serversCache.clear() + for (guid in serverList) { + val config = MmkvManager.decodeServerConfig(guid) ?: continue + if (subscriptionId.isNotEmpty() && subscriptionId != config.subscriptionId) { + continue + } + + if (keywordFilter.isEmpty() || config.remarks.contains(keywordFilter)) { + serversCache.add(ServersCache(guid, config)) + } + } + } + + fun testAllTcping() { + tcpingTestScope.coroutineContext[Job]?.cancelChildren() + SpeedtestUtil.closeAllTcpSockets() + MmkvManager.clearAllTestDelayResults() + updateListAction.value = -1 // update all + + getApplication().toast(R.string.connection_test_testing) + for (item in serversCache) { + item.config.getProxyOutbound()?.let { outbound -> + val serverAddress = outbound.getServerAddress() + val serverPort = outbound.getServerPort() + if (serverAddress != null && serverPort != null) { + tcpingTestScope.launch { + val testResult = SpeedtestUtil.tcping(serverAddress, serverPort) + launch(Dispatchers.Main) { + MmkvManager.encodeServerTestDelayMillis(item.guid, testResult) + updateListAction.value = getPosition(item.guid) + } + } + } + } + } + } + + fun testAllRealPing() { + MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "") + MmkvManager.clearAllTestDelayResults() + updateListAction.value = -1 // update all + + getApplication().toast(R.string.connection_test_testing) + viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread + for (item in serversCache) { + val config = V2rayConfigUtil.getV2rayConfig(getApplication(), item.guid) + if (config.status) { + MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, Pair(item.guid, config.content)) + } + } + } + } + + fun testCurrentServerRealPing() { + MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "") + } + + fun filterConfig(context :Context) { + val subscriptions = MmkvManager.decodeSubscriptions() + val listId = subscriptions.map { it.first }.toList().toMutableList() + val listRemarks = subscriptions.map { it.second.remarks }.toList().toMutableList() + listRemarks += context.getString(R.string.filter_config_all) + val checkedItem = if (subscriptionId.isNotEmpty()) { + listId.indexOf(subscriptionId) + } else { + listRemarks.count() - 1 + } + + val ivBinding = DialogConfigFilterBinding.inflate(LayoutInflater.from(context)) + ivBinding.spSubscriptionId.adapter = ArrayAdapter( context, android.R.layout.simple_spinner_dropdown_item, listRemarks) + ivBinding.spSubscriptionId.setSelection(checkedItem) + ivBinding.etKeyword.text = Utils.getEditable(keywordFilter) + val builder = AlertDialog.Builder(context).setView(ivBinding.root) + builder.setTitle(R.string.title_filter_config) + builder.setPositiveButton(R.string.tasker_setting_confirm) { dialogInterface: DialogInterface?, _: Int -> + try { + val position = ivBinding.spSubscriptionId.selectedItemPosition + subscriptionId = if (listRemarks.count() - 1 == position) { + "" + } else { + subscriptions[position].first + } + keywordFilter = ivBinding.etKeyword.text.toString() + reloadServerList() + + dialogInterface?.dismiss() + } catch (e: Exception) { + e.printStackTrace() + } + } + builder.show() +// AlertDialog.Builder(context) +// .setSingleChoiceItems(listRemarks.toTypedArray(), checkedItem) { dialog, i -> +// try { +// subscriptionId = if (listRemarks.count() - 1 == i) { +// "" +// } else { +// subscriptions[i].first +// } +// reloadServerList() +// dialog.dismiss() +// } catch (e: Exception) { +// e.printStackTrace() +// } +// }.show() + } + + fun getPosition(guid: String) : Int { + serversCache.forEachIndexed { index, it -> + if (it.guid == guid) + return index + } + return -1 + } + + private val mMsgReceiver = object : BroadcastReceiver() { + override fun onReceive(ctx: Context?, intent: Intent?) { + when (intent?.getIntExtra("key", 0)) { + AppConfig.MSG_STATE_RUNNING -> { + isRunning.value = true + } + AppConfig.MSG_STATE_NOT_RUNNING -> { + isRunning.value = false + } + AppConfig.MSG_STATE_START_SUCCESS -> { + getApplication().toast(R.string.toast_services_success) + isRunning.value = true + } + AppConfig.MSG_STATE_START_FAILURE -> { + getApplication().toast(R.string.toast_services_failure) + isRunning.value = false + } + AppConfig.MSG_STATE_STOP_SUCCESS -> { + isRunning.value = false + } + AppConfig.MSG_MEASURE_DELAY_SUCCESS -> { + updateTestResultAction.value = intent.getStringExtra("content") + } + AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> { + val resultPair = intent.getSerializableExtra("content") as Pair + MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second) + updateListAction.value = getPosition(resultPair.first) + } + } + } + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/SettingsViewModel.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/SettingsViewModel.kt new file mode 100644 index 00000000..2b40cc39 --- /dev/null +++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/SettingsViewModel.kt @@ -0,0 +1,64 @@ +package com.v2ray.ang.viewmodel + +import android.app.Application +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 + +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) + } + + override fun onCleared() { + PreferenceManager.getDefaultSharedPreferences(getApplication()).unregisterOnSharedPreferenceChangeListener(this) + Log.i(AppConfig.ANG_PACKAGE, "Settings ViewModel is cleared") + super.onCleared() + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { + Log.d(AppConfig.ANG_PACKAGE, "Observe settings changed: $key") + when(key) { + AppConfig.PREF_MODE, + AppConfig.PREF_VPN_DNS, + 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_LANGUAGE, + AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, + AppConfig.PREF_ROUTING_MODE, + AppConfig.PREF_V2RAY_ROUTING_AGENT, + AppConfig.PREF_V2RAY_ROUTING_BLOCKED, + AppConfig.PREF_V2RAY_ROUTING_DIRECT, -> { + settingsStorage?.encode(key, sharedPreferences.getString(key, "")) + } + AppConfig.PREF_SPEED_ENABLED, + AppConfig.PREF_PROXY_SHARING, + AppConfig.PREF_LOCAL_DNS_ENABLED, + AppConfig.PREF_FAKE_DNS_ENABLED, + AppConfig.PREF_ALLOW_INSECURE, + AppConfig.PREF_PREFER_IPV6, + AppConfig.PREF_PER_APP_PROXY, + AppConfig.PREF_BYPASS_APPS, + AppConfig.PREF_CONFIRM_REMOVE, -> { + settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false)) + } + AppConfig.PREF_SNIFFING_ENABLED -> { + settingsStorage?.encode(key, sharedPreferences.getBoolean(key, true)) + } + AppConfig.PREF_PER_APP_PROXY_SET -> { + settingsStorage?.encode(key, sharedPreferences.getStringSet(key, setOf())) + } + } + } +} diff --git a/V2rayNG/app/src/main/res/anim/fade_in.xml b/V2rayNG/app/src/main/res/anim/fade_in.xml new file mode 100644 index 00000000..29e04320 --- /dev/null +++ b/V2rayNG/app/src/main/res/anim/fade_in.xml @@ -0,0 +1,6 @@ + + diff --git a/V2rayNG/app/src/main/res/anim/fade_out.xml b/V2rayNG/app/src/main/res/anim/fade_out.xml new file mode 100644 index 00000000..2b8bb1cb --- /dev/null +++ b/V2rayNG/app/src/main/res/anim/fade_out.xml @@ -0,0 +1,6 @@ + + diff --git a/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name_black.png b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name_black.png deleted file mode 100644 index 01dc4ef9..00000000 Binary files a/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name_black.png and /dev/null differ diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_about_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_about_24dp.xml deleted file mode 100644 index d256a2a5..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_about_24dp.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_action_done.xml b/V2rayNG/app/src/main/res/drawable-night/ic_action_done.xml deleted file mode 100644 index c3fb8a47..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_action_done.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_add_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_add_24dp.xml deleted file mode 100644 index d2948026..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_add_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_backup_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_backup_24dp.xml deleted file mode 100644 index 9f774c85..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_backup_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_check_update_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_check_update_24dp.xml deleted file mode 100644 index 5a7c2fd0..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_check_update_24dp.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_edit_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_edit_24dp.xml deleted file mode 100644 index 85d32985..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_edit_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_fab_check.xml b/V2rayNG/app/src/main/res/drawable-night/ic_fab_check.xml deleted file mode 100644 index 54f825f8..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_fab_check.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_feedback_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_feedback_24dp.xml deleted file mode 100644 index 212bc072..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_feedback_24dp.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_file_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_file_24dp.xml deleted file mode 100644 index 73387126..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_file_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_image_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_image_24dp.xml deleted file mode 100644 index 00449aaf..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_image_24dp.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - 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 deleted file mode 100644 index 48309ad9..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_lock_24dp.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_logcat_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_logcat_24dp.xml deleted file mode 100644 index 77e41a46..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_logcat_24dp.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_more_vert_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_more_vert_24dp.xml deleted file mode 100644 index 2a252346..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_more_vert_24dp.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_outline_filter_alt_24.xml b/V2rayNG/app/src/main/res/drawable-night/ic_outline_filter_alt_24.xml deleted file mode 100644 index 07553f4e..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_outline_filter_alt_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_per_apps_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_per_apps_24dp.xml deleted file mode 100644 index 99d8212e..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_per_apps_24dp.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_play_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_play_24dp.xml deleted file mode 100644 index a3bc803f..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_play_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_privacy_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_privacy_24dp.xml deleted file mode 100644 index 1cec1d77..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_privacy_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_promotion_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_promotion_24dp.xml deleted file mode 100644 index e6142817..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_promotion_24dp.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_restore_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_restore_24dp.xml deleted file mode 100644 index 6b372816..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_restore_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - 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 deleted file mode 100644 index 7002c085..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_routing_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_scan_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_scan_24dp.xml deleted file mode 100644 index 9e260e7d..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_scan_24dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_settings_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_settings_24dp.xml deleted file mode 100644 index 49347d0d..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_settings_24dp.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_source_code_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_source_code_24dp.xml deleted file mode 100644 index 43b540fb..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_source_code_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_stop_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_stop_24dp.xml deleted file mode 100644 index d3fbe696..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_stop_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_subscriptions_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_subscriptions_24dp.xml deleted file mode 100644 index e935b87d..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_subscriptions_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_telegram_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_telegram_24dp.xml deleted file mode 100644 index b35b623c..00000000 --- a/V2rayNG/app/src/main/res/drawable-night/ic_telegram_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/nav_header_bg.png b/V2rayNG/app/src/main/res/drawable-night/nav_header_bg.png deleted file mode 100644 index 82cb00af..00000000 Binary files a/V2rayNG/app/src/main/res/drawable-night/nav_header_bg.png and /dev/null differ diff --git a/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name_black.png b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name_black.png deleted file mode 100644 index 00207a53..00000000 Binary files a/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name_black.png and /dev/null differ diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/donate.png b/V2rayNG/app/src/main/res/drawable-xxhdpi/donate.png new file mode 100644 index 00000000..8825c532 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxhdpi/donate.png differ diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name_black.png b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name_black.png deleted file mode 100644 index 516f0893..00000000 Binary files a/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name_black.png and /dev/null differ diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/side_nav_bar.xml b/V2rayNG/app/src/main/res/drawable-xxhdpi/side_nav_bar.xml new file mode 100644 index 00000000..6d81870b --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-xxhdpi/side_nav_bar.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name_black.png b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name_black.png deleted file mode 100644 index a18be25c..00000000 Binary files a/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name_black.png and /dev/null differ diff --git a/V2rayNG/app/src/main/res/drawable/custom_divider.xml b/V2rayNG/app/src/main/res/drawable/custom_divider.xml deleted file mode 100644 index 94f03147..00000000 --- a/V2rayNG/app/src/main/res/drawable/custom_divider.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_about_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_about_24dp.xml deleted file mode 100644 index 5e1ac138..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_about_24dp.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_action_done.xml b/V2rayNG/app/src/main/res/drawable/ic_action_done.xml index bda675f1..33a117f6 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_action_done.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_action_done.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:fillColor="#FFFFFFFF" + android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/> diff --git a/V2rayNG/app/src/main/res/drawable/ic_add_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_add_24dp.xml deleted file mode 100644 index fedd077d..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_add_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_add_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_add_white_24dp.xml new file mode 100644 index 00000000..b9b8eca8 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_add_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_attach_money_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_attach_money_black_24dp.xml new file mode 100644 index 00000000..b520fc98 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_attach_money_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_attach_money_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_attach_money_white_24dp.xml new file mode 100644 index 00000000..2b65f0c6 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_attach_money_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_backup_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_backup_24dp.xml deleted file mode 100644 index 0eca2402..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_backup_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_check_update_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_check_update_24dp.xml deleted file mode 100644 index b0f163c6..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_check_update_24dp.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_circle.xml b/V2rayNG/app/src/main/res/drawable/ic_circle.xml deleted file mode 100644 index 13bb8e65..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_circle.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_close_grey_800_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_close_grey_800_24dp.xml new file mode 100644 index 00000000..8a9a5226 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_close_grey_800_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_cloud_download_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_cloud_download_24dp.xml deleted file mode 100644 index aebcd40a..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_cloud_download_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_cloud_download_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_cloud_download_white_24dp.xml similarity index 100% rename from V2rayNG/app/src/main/res/drawable-night/ic_cloud_download_24dp.xml rename to V2rayNG/app/src/main/res/drawable/ic_cloud_download_white_24dp.xml diff --git a/V2rayNG/app/src/main/res/drawable/ic_copy.xml b/V2rayNG/app/src/main/res/drawable/ic_copy.xml deleted file mode 100644 index 380c8783..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_copy.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_copy.xml b/V2rayNG/app/src/main/res/drawable/ic_copy_white.xml similarity index 55% rename from V2rayNG/app/src/main/res/drawable-night/ic_copy.xml rename to V2rayNG/app/src/main/res/drawable/ic_copy_white.xml index 9cafc852..e50927b2 100644 --- a/V2rayNG/app/src/main/res/drawable-night/ic_copy.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_copy_white.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/> diff --git a/V2rayNG/app/src/main/res/drawable/ic_delete_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_delete_black_24dp.xml similarity index 61% rename from V2rayNG/app/src/main/res/drawable/ic_delete_24dp.xml rename to V2rayNG/app/src/main/res/drawable/ic_delete_black_24dp.xml index c3027b2e..2f5557af 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_delete_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_delete_black_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/> diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_delete_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_delete_white_24dp.xml similarity index 61% rename from V2rayNG/app/src/main/res/drawable-night/ic_delete_24dp.xml rename to V2rayNG/app/src/main/res/drawable/ic_delete_white_24dp.xml index 4c803eca..ab38bb6d 100644 --- a/V2rayNG/app/src/main/res/drawable-night/ic_delete_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_delete_white_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/> diff --git a/V2rayNG/app/src/main/res/drawable/ic_description_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_description_black_24dp.xml similarity index 56% rename from V2rayNG/app/src/main/res/drawable/ic_description_24dp.xml rename to V2rayNG/app/src/main/res/drawable/ic_description_black_24dp.xml index a30bc6ca..38c33351 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_description_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_description_black_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z"/> diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_description_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_description_white_24dp.xml similarity index 56% rename from V2rayNG/app/src/main/res/drawable-night/ic_description_24dp.xml rename to V2rayNG/app/src/main/res/drawable/ic_description_white_24dp.xml index 6f55b8de..7e0d28e3 100644 --- a/V2rayNG/app/src/main/res/drawable-night/ic_description_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_description_white_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z"/> diff --git a/V2rayNG/app/src/main/res/drawable/ic_edit_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_edit_black_24dp.xml similarity index 69% rename from V2rayNG/app/src/main/res/drawable/ic_edit_24dp.xml rename to V2rayNG/app/src/main/res/drawable/ic_edit_black_24dp.xml index 5505cc77..2ab2fb75 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_edit_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_edit_black_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> diff --git a/V2rayNG/app/src/main/res/drawable/ic_fab_check.xml b/V2rayNG/app/src/main/res/drawable/ic_fab_check.xml index 6c1b1677..54f825f8 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_fab_check.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_fab_check.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/V2rayNG/app/src/main/res/drawable/ic_feedback_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_feedback_24dp.xml deleted file mode 100644 index 58d18767..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_feedback_24dp.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_feedback_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_feedback_white_24dp.xml new file mode 100644 index 00000000..3e08fae2 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_feedback_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_file_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_file_24dp.xml deleted file mode 100644 index 50ca7039..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_file_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_file_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_file_white_24dp.xml new file mode 100644 index 00000000..f7f9df2e --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_file_white_24dp.xml @@ -0,0 +1,34 @@ + + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_image_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_image_24dp.xml deleted file mode 100644 index e2ff6078..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_image_24dp.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_image_photo.xml b/V2rayNG/app/src/main/res/drawable/ic_image_photo.xml new file mode 100644 index 00000000..c701a63f --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_image_photo.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_info_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_info_black_24dp.xml new file mode 100644 index 00000000..34b8202e --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_info_black_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 deleted file mode 100644 index da0e84b4..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_lock_24dp.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_logcat_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_logcat_24dp.xml deleted file mode 100644 index 95fe140c..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_logcat_24dp.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_logcat_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_logcat_white_24dp.xml new file mode 100644 index 00000000..e7d3eb31 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_logcat_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_more_vert_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_more_vert_24dp.xml deleted file mode 100644 index 32d39196..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_more_vert_24dp.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_outline_filter_alt_24.xml b/V2rayNG/app/src/main/res/drawable/ic_outline_filter_alt_24.xml deleted file mode 100644 index 77112182..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_outline_filter_alt_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_outline_filter_alt_white_24.xml b/V2rayNG/app/src/main/res/drawable/ic_outline_filter_alt_white_24.xml new file mode 100644 index 00000000..6cb82eff --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_outline_filter_alt_white_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_per_apps_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_per_apps_24dp.xml deleted file mode 100644 index c45cd8c5..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_per_apps_24dp.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_play_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_play_24dp.xml deleted file mode 100644 index a3bc803f..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_play_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_privacy_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_privacy_24dp.xml deleted file mode 100644 index ad1a12d7..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_privacy_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_promotion_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_promotion_24dp.xml deleted file mode 100644 index f4283b58..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_promotion_24dp.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_scan_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_scan_24dp.xml deleted file mode 100644 index 2435e3b8..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_qu_scan_24dp.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_scan_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_scan_black_24dp.xml new file mode 100644 index 00000000..e0b32bf4 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_qu_scan_black_24dp.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_settings_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_settings_black_24dp.xml new file mode 100644 index 00000000..063171a3 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_qu_settings_black_24dp.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_switch_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_switch_24dp.xml deleted file mode 100644 index 79be6f81..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_qu_switch_24dp.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_switch_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_switch_black_24dp.xml new file mode 100644 index 00000000..97f8859d --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_qu_switch_black_24dp.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_restore_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_restore_24dp.xml deleted file mode 100644 index 12d78b2c..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_restore_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_active.xml b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_active.xml deleted file mode 100644 index 3dcd95f7..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_active.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_grey.xml b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_grey.xml new file mode 100644 index 00000000..50af166d --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_grey.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_inactive.xml b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_inactive.xml deleted file mode 100644 index 96a98158..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_inactive.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_theme.xml b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_theme.xml new file mode 100644 index 00000000..0463d8bf --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_theme.xml @@ -0,0 +1,6 @@ + + + + + + \ 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 deleted file mode 100644 index 9032a2e6..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_routing_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_save_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_save_24dp.xml deleted file mode 100644 index 0651fcc6..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_save_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_save_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_save_white_24dp.xml similarity index 69% rename from V2rayNG/app/src/main/res/drawable-night/ic_save_24dp.xml rename to V2rayNG/app/src/main/res/drawable/ic_save_white_24dp.xml index 47a92519..a7a81a25 100644 --- a/V2rayNG/app/src/main/res/drawable-night/ic_save_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_save_white_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/> diff --git a/V2rayNG/app/src/main/res/drawable/ic_scan_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_scan_24dp.xml deleted file mode 100644 index f7cc6c32..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_scan_24dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_scan_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_scan_black_24dp.xml new file mode 100644 index 00000000..a31063ba --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_scan_black_24dp.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_select_all_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_select_all_24dp.xml deleted file mode 100644 index f923b7e8..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_select_all_24dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_select_all_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_select_all_white_24dp.xml similarity index 91% rename from V2rayNG/app/src/main/res/drawable-night/ic_select_all_24dp.xml rename to V2rayNG/app/src/main/res/drawable/ic_select_all_white_24dp.xml index 3d37d197..a24c01bf 100644 --- a/V2rayNG/app/src/main/res/drawable-night/ic_select_all_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_select_all_white_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp"> diff --git a/V2rayNG/app/src/main/res/drawable/ic_settings_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_settings_24dp.xml deleted file mode 100644 index 9bda1725..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_settings_24dp.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_settings_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_settings_white_24dp.xml new file mode 100644 index 00000000..ce997a72 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_settings_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_share_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_share_black_24dp.xml similarity index 79% rename from V2rayNG/app/src/main/res/drawable/ic_share_24dp.xml rename to V2rayNG/app/src/main/res/drawable/ic_share_black_24dp.xml index 338d95ad..e3fe874d 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_share_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_share_black_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/> diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_share_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_share_white_24dp.xml similarity index 79% rename from V2rayNG/app/src/main/res/drawable-night/ic_share_24dp.xml rename to V2rayNG/app/src/main/res/drawable/ic_share_white_24dp.xml index b09d63e8..90406663 100644 --- a/V2rayNG/app/src/main/res/drawable-night/ic_share_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_share_white_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/> diff --git a/V2rayNG/app/src/main/res/drawable/ic_shortcut_background.xml b/V2rayNG/app/src/main/res/drawable/ic_shortcut_background.xml new file mode 100644 index 00000000..a62b720b --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_shortcut_background.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_source_code_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_source_code_24dp.xml deleted file mode 100644 index 800f7b5f..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_source_code_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_stop_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_stop_24dp.xml deleted file mode 100644 index d3fbe696..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_stop_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_subscriptions_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_24dp.xml deleted file mode 100644 index 9e82c7f9..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_subscriptions_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_subscriptions_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_black_24dp.xml new file mode 100644 index 00000000..6f0ed455 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_subscriptions_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_white_24dp.xml new file mode 100644 index 00000000..bc20a83a --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_telegram_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_telegram_24dp.xml deleted file mode 100644 index f1f8fa5a..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_telegram_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_whatshot_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_whatshot_black_24dp.xml new file mode 100644 index 00000000..1cbc037f --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_whatshot_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_whatshot_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_whatshot_white_24dp.xml new file mode 100644 index 00000000..ad460f3c --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_whatshot_white_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 deleted file mode 100644 index 5b63fd29..00000000 --- a/V2rayNG/app/src/main/res/drawable/license_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/nav_header_bg.png b/V2rayNG/app/src/main/res/drawable/nav_header_bg.png index fd79d42d..82cb00af 100644 Binary files a/V2rayNG/app/src/main/res/drawable/nav_header_bg.png and b/V2rayNG/app/src/main/res/drawable/nav_header_bg.png differ diff --git a/V2rayNG/app/src/main/res/font/montserrat_thin.ttf b/V2rayNG/app/src/main/res/font/montserrat_thin.ttf deleted file mode 100644 index 4c5c8f7e..00000000 Binary files a/V2rayNG/app/src/main/res/font/montserrat_thin.ttf and /dev/null differ diff --git a/V2rayNG/app/src/main/res/layout/activity_about.xml b/V2rayNG/app/src/main/res/layout/activity_about.xml deleted file mode 100644 index 62053559..00000000 --- a/V2rayNG/app/src/main/res/layout/activity_about.xml +++ /dev/null @@ -1,255 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml index 0fc67600..9baff9a2 100644 --- a/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml +++ b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml @@ -4,24 +4,15 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:fitsSystemWindows="true" - android:orientation="vertical" - tools:context=".ui.PerAppProxyActivity"> - - + android:orientation="vertical"> + android:paddingStart="20dp" + android:paddingEnd="20dp"> + + + android:paddingStart="20dp" /> @@ -63,31 +56,18 @@ android:gravity="center" android:orientation="horizontal"> + + - - - - - - + android:paddingStart="20dp" /> @@ -96,11 +76,17 @@ + + + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:context=".ui.PerAppProxyActivity" /> \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_check_update.xml b/V2rayNG/app/src/main/res/layout/activity_check_update.xml deleted file mode 100644 index 29345075..00000000 --- a/V2rayNG/app/src/main/res/layout/activity_check_update.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/V2rayNG/app/src/main/res/layout/activity_logcat.xml b/V2rayNG/app/src/main/res/layout/activity_logcat.xml index 05db70a7..06916be1 100644 --- a/V2rayNG/app/src/main/res/layout/activity_logcat.xml +++ b/V2rayNG/app/src/main/res/layout/activity_logcat.xml @@ -1,26 +1,38 @@ + android:padding="10dp" + tools:context=".ui.MainActivity"> - + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" /> - + + - - - + android:layout_height="wrap_content" + android:gravity="bottom" + android:maxLines="65536" + android:textAppearance="?android:attr/textAppearanceSmall" /> + diff --git a/V2rayNG/app/src/main/res/layout/activity_main.xml b/V2rayNG/app/src/main/res/layout/activity_main.xml index e5f6964c..5f3c9e95 100644 --- a/V2rayNG/app/src/main/res/layout/activity_main.xml +++ b/V2rayNG/app/src/main/res/layout/activity_main.xml @@ -3,17 +3,18 @@ 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:layout_height="match_parent" + android:fitsSystemWindows="true"> + android:layout_height="wrap_content" + android:theme="@style/AppTheme.AppBarOverlay"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> - - - - + android:nextFocusRight="@+id/fab" /> - - + android:nextFocusLeft="@+id/recycler_view"> + android:textAppearance="@style/TextAppearance.AppCompat.Small" + android:textColor="@color/colorWhite" /> - + android:layout_marginBottom="24dp"> - + @@ -119,9 +106,20 @@ android:layout_height="match_parent" android:layout_gravity="start" app:headerLayout="@layout/nav_header" - app:itemIconTint="@color/colorAccent" - app:menu="@menu/menu_drawer"> + app:menu="@menu/menu_drawer" > + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_none.xml b/V2rayNG/app/src/main/res/layout/activity_none.xml index c7d9548b..d7681698 100644 --- a/V2rayNG/app/src/main/res/layout/activity_none.xml +++ b/V2rayNG/app/src/main/res/layout/activity_none.xml @@ -1,4 +1,5 @@ + android:layout_height="match_parent"> + diff --git a/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml b/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml deleted file mode 100644 index 53a49fec..00000000 --- a/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml +++ /dev/null @@ -1,193 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 deleted file mode 100644 index d79b4c84..00000000 --- a/V2rayNG/app/src/main/res/layout/activity_routing_setting.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ 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 new file mode 100644 index 00000000..7052cc5f --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_routing_settings.xml @@ -0,0 +1,17 @@ + + + + + + + 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 d6d587b8..2f1aefc9 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,8 +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"> + tools:context=".ui.ServerActivity"> + android:orientation="vertical" + android:layout_margin="@dimen/layout_margin_spacing"> + android:textColor="@color/fab_orange_dark" /> + android:orientation="vertical" + android:layout_margin="@dimen/layout_margin_spacing"> @@ -65,16 +64,16 @@ + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="top|start" + android:id="@+id/editor" + android:layout_marginTop="@dimen/layout_margin_top_height" /> diff --git a/V2rayNG/app/src/main/res/layout/activity_server_hysteria2.xml b/V2rayNG/app/src/main/res/layout/activity_server_hysteria2.xml deleted file mode 100644 index 86df50ef..00000000 --- a/V2rayNG/app/src/main/res/layout/activity_server_hysteria2.xml +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 0d9e958f..dba1c142 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml @@ -3,21 +3,86 @@ 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:padding="@dimen/layout_margin_top_height"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -37,7 +102,7 @@ - - - - + \ No newline at end of file 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 90c1b883..5f34c37c 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_socks.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_socks.xml @@ -3,21 +3,86 @@ 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:padding="@dimen/layout_margin_top_height"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -37,7 +102,7 @@ + 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 2159ec43..757a6c78 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_trojan.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_trojan.xml @@ -3,21 +3,86 @@ 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:padding="@dimen/layout_margin_top_height"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 7d29efd2..8e70bd02 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_vless.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_vless.xml @@ -3,21 +3,86 @@ 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:padding="@dimen/layout_margin_top_height"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -36,7 +101,7 @@ + + android:id="@+id/et_security" + android:layout_width="match_parent" + android:layout_height="@dimen/edit_height" + android:inputType="text" + android:text="none" /> - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 4e77cbdf..ebfeb7e1 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml @@ -3,21 +3,86 @@ 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:padding="@dimen/layout_margin_top_height"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -36,7 +101,27 @@ + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_server_wireguard.xml b/V2rayNG/app/src/main/res/layout/activity_server_wireguard.xml deleted file mode 100644 index 7ecd7ce1..00000000 --- a/V2rayNG/app/src/main/res/layout/activity_server_wireguard.xml +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_settings.xml b/V2rayNG/app/src/main/res/layout/activity_settings.xml index 618bc8c0..f674f2ae 100644 --- a/V2rayNG/app/src/main/res/layout/activity_settings.xml +++ b/V2rayNG/app/src/main/res/layout/activity_settings.xml @@ -1,16 +1,8 @@ - - - - - \ No newline at end of file + 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 af5d70cd..770f93e4 100644 --- a/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml +++ b/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml @@ -1,11 +1,9 @@ + tools:context=".ui.ServerActivity"> + android:padding="@dimen/layout_margin_top_height"> @@ -52,7 +50,29 @@ + + + + + + + + + + android:minLines="5" + android:scrollbars="vertical" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/V2rayNG/app/src/main/res/layout/activity_sub_setting.xml b/V2rayNG/app/src/main/res/layout/activity_sub_setting.xml index 1654701d..bb87f387 100644 --- a/V2rayNG/app/src/main/res/layout/activity_sub_setting.xml +++ b/V2rayNG/app/src/main/res/layout/activity_sub_setting.xml @@ -1,25 +1,22 @@ + + tools:context=".ui.MainActivity"> - - - + android:layout_height="match_parent"> + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_tasker.xml b/V2rayNG/app/src/main/res/layout/activity_tasker.xml index 56a1276d..5c798c37 100644 --- a/V2rayNG/app/src/main/res/layout/activity_tasker.xml +++ b/V2rayNG/app/src/main/res/layout/activity_tasker.xml @@ -1,32 +1,31 @@ + android:layout_height="@dimen/bypass_list_header_height"> + android:checked="true" /> diff --git a/V2rayNG/app/src/main/res/layout/activity_user_asset.xml b/V2rayNG/app/src/main/res/layout/activity_user_asset.xml deleted file mode 100644 index 08d7583f..00000000 --- a/V2rayNG/app/src/main/res/layout/activity_user_asset.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file 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 deleted file mode 100644 index e1b55173..00000000 --- a/V2rayNG/app/src/main/res/layout/activity_user_asset_url.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/dialog_config_filter.xml b/V2rayNG/app/src/main/res/layout/dialog_config_filter.xml index 0bbc9003..115371df 100644 --- a/V2rayNG/app/src/main/res/layout/dialog_config_filter.xml +++ b/V2rayNG/app/src/main/res/layout/dialog_config_filter.xml @@ -7,27 +7,25 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:padding="@dimen/padding_spacing_dp16"> + android:padding="@dimen/layout_margin_top_height"> + android:layout_height="@dimen/edit_height"/> diff --git a/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml b/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml new file mode 100644 index 00000000..45f2a305 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/item_qrcode.xml b/V2rayNG/app/src/main/res/layout/item_qrcode.xml index d3ea9075..3c53162a 100644 --- a/V2rayNG/app/src/main/res/layout/item_qrcode.xml +++ b/V2rayNG/app/src/main/res/layout/item_qrcode.xml @@ -1,6 +1,5 @@ + android:src="@drawable/ic_fab_check" /> 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 889cb3c6..03ad10a7 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 @@ -2,47 +2,51 @@ + android:background="?android:attr/selectableItemBackground" + android:clickable="true" + android:focusable="true"> + android:layout_width="60dp" + android:layout_height="60dp" + android:paddingStart="10dp" + android:paddingLeft="10dp" + android:paddingEnd="10dp" + android:paddingRight="10dp" /> + android:orientation="vertical" + android:paddingStart="@dimen/layout_margin_right_height" + android:paddingEnd="@dimen/layout_margin_right_height"> + android:layout_height="wrap_content" /> + + android:paddingStart="2dp" + android:paddingLeft="2dp" + android:paddingEnd="6dp" + android:paddingRight="6dp" /> \ 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 01db7a96..268f7690 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml @@ -8,25 +8,25 @@ + android:padding="5dp"> + android:orientation="vertical" + android:paddingBottom="@dimen/layout_margin_top_height"> + android:textColor="@color/fab_orange_dark" /> \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_logcat.xml b/V2rayNG/app/src/main/res/layout/item_recycler_logcat.xml deleted file mode 100644 index 73d4e23d..00000000 --- a/V2rayNG/app/src/main/res/layout/item_recycler_logcat.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - \ 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 313492d4..acae822a 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_main.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_main.xml @@ -1,219 +1,189 @@ - - - - + android:gravity="center_vertical"> + + + android:layout_height="@dimen/server_height" + android:layout_gravity="center" + android:gravity="center" + android:orientation="horizontal"> + + + android:layout_weight="1" + android:orientation="vertical" + android:paddingStart="5dp"> - - - - - - - - - - - - - - - - - - - - + android:maxLines="2" + android:minLines="1" + android:textAppearance="@style/TextAppearance.AppCompat.Subhead" /> + + + + + + + + + + + + + + + + + + android:padding="@dimen/layout_margin_spacing"> + android:layout_height="@dimen/png_height" + android:src="@drawable/ic_share_black_24dp" + app:tint="?attr/colorMainText" /> + android:padding="@dimen/layout_margin_spacing"> + android:layout_width="@dimen/png_height" + android:layout_height="@dimen/png_height" + android:src="@drawable/ic_edit_black_24dp" + app:tint="?attr/colorMainText" /> + android:padding="@dimen/layout_margin_spacing"> - - - - - - + android:layout_width="@dimen/png_height" + android:layout_height="@dimen/png_height" + android:src="@drawable/ic_delete_black_24dp" + app:tint="?attr/colorMainText" /> - - - - - - - + android:layout_gravity="right" + android:orientation="vertical" + android:paddingEnd="5dp"> + + + - - - + diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_routing_setting.xml b/V2rayNG/app/src/main/res/layout/item_recycler_routing_setting.xml deleted file mode 100644 index e104ef97..00000000 --- a/V2rayNG/app/src/main/res/layout/item_recycler_routing_setting.xml +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 47c2b4c5..a4467e27 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 @@ -6,153 +6,104 @@ android:layout_height="wrap_content" android:gravity="center_vertical"> - + card_view:cardCornerRadius="5dp"> + android:layout_height="@dimen/server_height" + android:layout_gravity="center" + android:gravity="center" + android:orientation="horizontal"> + + + android:layout_weight="1" + android:paddingStart="5dp" + android:orientation="vertical"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_marginTop="10dp" + android:lines="1" + android:textAppearance="@style/TextAppearance.AppCompat.Small" /> + - - - - - - + android:layout_width="wrap_content" + android:layout_height="@dimen/server_height" + android:gravity="center" + android:orientation="horizontal"> + android:background="?android:attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:gravity="center" + android:orientation="vertical" + android:padding="@dimen/layout_margin_spacing" + android:visibility="invisible"> + + - + + + + + - - - \ No newline at end of file + + 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 61cd2a67..54651771 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 @@ -1,86 +1,60 @@ - - + android:layout_height="wrap_content"> + + + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center" + android:padding="@dimen/nav_header_vertical_spacing"> - - + android:layout_weight="1" + android:orientation="vertical"> + android:id="@+id/asset_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.AppCompat.Subhead" + tools:text="Placeholder.dat" /> - - + android:id="@+id/asset_properties" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAlignment="textEnd" + android:textAppearance="@style/TextAppearance.AppCompat.Small" + tools:text="1MB . 2020.01.01" /> + + + + - - - - - - - - - - - - + diff --git a/V2rayNG/app/src/main/res/layout/layout_address_port.xml b/V2rayNG/app/src/main/res/layout/layout_address_port.xml deleted file mode 100644 index 1c0111ca..00000000 --- a/V2rayNG/app/src/main/res/layout/layout_address_port.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/layout_tls.xml b/V2rayNG/app/src/main/res/layout/layout_tls.xml deleted file mode 100644 index b7c34a12..00000000 --- a/V2rayNG/app/src/main/res/layout/layout_tls.xml +++ /dev/null @@ -1,181 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/layout_tls_hysteria2.xml b/V2rayNG/app/src/main/res/layout/layout_tls_hysteria2.xml deleted file mode 100644 index fc279bcb..00000000 --- a/V2rayNG/app/src/main/res/layout/layout_tls_hysteria2.xml +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 deleted file mode 100644 index 85712ff8..00000000 --- a/V2rayNG/app/src/main/res/layout/layout_transport.xml +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 44a175d2..2f5d3ed6 100644 --- a/V2rayNG/app/src/main/res/layout/nav_header.xml +++ b/V2rayNG/app/src/main/res/layout/nav_header.xml @@ -1,20 +1,24 @@ + android:paddingBottom="@dimen/activity_vertical_margin" + android:paddingLeft="@dimen/activity_horizontal_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingTop="@dimen/activity_vertical_margin"> diff --git a/V2rayNG/app/src/main/res/layout/nav_toolbar.xml b/V2rayNG/app/src/main/res/layout/nav_toolbar.xml new file mode 100644 index 00000000..262787df --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/nav_toolbar.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/nav_view.xml b/V2rayNG/app/src/main/res/layout/nav_view.xml new file mode 100644 index 00000000..34fd03a3 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/nav_view.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/preference_with_help_link.xml b/V2rayNG/app/src/main/res/layout/preference_with_help_link.xml index 6d21827b..ec6e6423 100644 --- a/V2rayNG/app/src/main/res/layout/preference_with_help_link.xml +++ b/V2rayNG/app/src/main/res/layout/preference_with_help_link.xml @@ -2,10 +2,10 @@