Compare commits

..

71 Commits
1.9.4 ... 1.9.9

Author SHA1 Message Date
2dust
e60eb703c6 up 1.9.9 2024-10-28 13:54:49 +08:00
2dust
cdede639f2 Adjustment of preset rule sets 2024-10-28 13:45:00 +08:00
2dust
23264d71f0 Test connection after updating subscription 2024-10-28 12:05:17 +08:00
2dust
9caafc4303 Add toast insecure protocol 2024-10-27 20:44:57 +08:00
Tamim Hossain
48a3690a39 refactor steam visiblity (#3786)
refactor steam visiblity
2024-10-25 20:07:43 +08:00
Tamim Hossain
b66a8ca44d Refactor QRCodeDecoder for readability and performance (#3782)
- Improved the `createQRCode` function by replacing manual loops with Kotlin idioms and using `runCatching` for safer error handling.
- Refactored `syncDecodeQRCode` to simplify control flow and avoid redundant null checks.
- Enhanced error handling and logging by using `runCatching` and more concise exception handling.
2024-10-25 19:19:22 +08:00
Tamim Hossain
82087e1187 Refactor UserAssetActivity for readability and efficiency (#3781)
- Refactored the onOptionsItemSelected function to use `when` and `let` for cleaner conditional logic.
- Simplified the `chooseFile` function with `runCatching` for better error handling.
- Extracted repeated code into reusable functions to improve maintainability.
- Improved coroutine handling by ensuring file I/O happens on the IO thread.
2024-10-25 19:17:35 +08:00
Tamim Hossain
fdfd0438c3 Refactor updateMuxConcurrency for Null Safety and Code Simplification (#3780)
Refactored the updateMuxConcurrency function by removing redundant null checks and improving code readability. The concurrency value now defaults to 8 if null or invalid input is provided.
2024-10-25 19:14:28 +08:00
Tamim Hossain
28027b5288 Fix Comment and Improve Null Safety in ServerCustomConfigActivity (#3779)
Fixed the comment typo in ServerCustomConfigActivity and improved null safety in the bindingServer function by ensuring proper fallback for raw content. Simplified logic for setting editor content.
2024-10-25 19:13:34 +08:00
Tamim Hossain
ba54005753 Improve Permission Handling and QR Code Import Logic in ScScannerActivity (#3777)
Refactored permission request handling in ScScannerActivity to improve readability and simplify the flow. Improved QR code import logic to ensure better handling of the scanned result and enhance user feedback.
2024-10-25 19:10:14 +08:00
Tamim Hossain
c6758b11b5 Improve Permission Request and QR Code Decoding in ScannerActivity (#3776)
Streamlined the permission request process using RxPermissions and improved error handling for file selection and QR code decoding in ScannerActivity. Enhanced user feedback for decoding failures and file processing errors.
2024-10-25 19:09:41 +08:00
Tamim Hossain
6c29e5e9a4 Optimize refreshData in RoutingSettingActivity (#3775)
Optimized the refreshData method by clearing the existing rulesets list and adding the new items to avoid unnecessary reallocation and improve memory management.
2024-10-25 19:07:55 +08:00
Tamim Hossain
17e0db2ffc Refactor Import Rulesets from Clipboard (#3774)
Refactored the import_rulesets_from_clipboard method in RoutingSettingActivity. Improved error handling by isolating the clipboard fetching inside a try-catch block and ensuring the routing ruleset reset logic is handled more cleanly.
2024-10-25 19:06:44 +08:00
Tamim Hossain
490ea59499 Refactor saveServer Method to Improve Null-Safety and Readability (#3773)
Refactored the saveServer method in RoutingEditActivity to improve null-safety and readability by using apply and takeIf. This ensures cleaner and more concise code while processing user inputs like domain, ip, protocol, and network.
2024-10-25 19:02:52 +08:00
Tamim Hossain
f4db6bcf63 Refactor Binding Logic in AppViewHolder of PerAppProxyAdapter (#3772)
Simplified the binding logic in AppViewHolder by improving readability and removing redundant code. Consolidated logic for setting the app name and handling system apps. This refactor improves the maintainability of the PerAppProxyAdapter class.
2024-10-25 19:01:56 +08:00
Tamim Hossain
90f89de957 Improve Error Handling in Batch Config Import and Progress Bar Management (#3771)
Enhanced error handling in the importBatchConfig method by adding a try-catch block. Ensured that the progress bar is hidden in both success and failure cases. Replaced nested if statements with a cleaner when clause for better readability.
2024-10-25 18:58:20 +08:00
Tamim Hossain
9612b868f2 Improve Error Handling in LogcatActivity (#3770)
Refined the error handling in the logcat function of LogcatActivity by ensuring that the progress bar is hidden in both success and failure cases. Added user-friendly error messages using toast and cleaned up the code by using Kotlin's linkedSetOf for the logcat command list.
2024-10-25 18:56:59 +08:00
Tamim Hossain
5f8ea93f36 Refactor attachBaseContext for Null Safety and Clean Code (#3769)
Simplified the attachBaseContext function by removing redundant let block and ensuring null safety with concise null handling. This refactor improves readability and ensures that null cases are handled directly.
2024-10-25 18:55:56 +08:00
Tamim Hossain
fc132f7282 Improve Exception Handling in File Chooser and Restore Process (#3768)
Enhanced exception handling in the file chooser and restore configuration process by adding detailed logging with Log.e(). Simplified the intent creation in showFileChooser and combined nested try-catch blocks for better readability and error management.
2024-10-25 18:36:17 +08:00
Tamim Hossain
6f9bb6caa7 Improve RxJava Disposable Handling in V2RayServiceManager (#3767)
Refactored the RxJava disposable handling in the V2RayServiceManager to ensure proper disposal when the service stops. This change prevents potential memory leaks by disposing of the disposable only when it's initialized, using a more concise and reliable approach with Kotlin's let function.
2024-10-25 18:35:03 +08:00
Tamim Hossain
0e5b88de8f remove parentheses (#3766)
remove parentheses
2024-10-25 18:34:19 +08:00
any116
5b4f51981e Fallback go ver for normal use (#3758)
* Update build.yml

* Delete .github/dependabot.yml
2024-10-24 09:14:38 +08:00
DecorativeFamily
3dcee45e9f Update strings.xml (#3741)
* Update strings.xml

update persian strings

* Update strings.xml

update persian strings

* Update strings.xml

update persian strings

* Update strings.xml

update persian strings
2024-10-23 09:14:27 +08:00
Tamim Hossain
3573a3bec3 Remove unnecessary null check from subscriptionId (#3739)
- Replaced `subscriptionId.isNullOrEmpty()` with `subscriptionId.isEmpty()` since `subscriptionId` is not nullable.
- This refactor simplifies the logic and improves code readability by eliminating the redundant null check.
2024-10-22 20:22:58 +08:00
Tamim Hossain
796bad1c1c Remove redundant qualifier from RulesBean initialization (#3738)
- Removed the redundant qualifier `V2rayConfig.RoutingBean.RulesBean` in favor of directly using `RulesBean`.
- This simplifies the code and improves readability by removing unnecessary fully qualified names.
- The change ensures cleaner and more maintainable code without altering functionality.
2024-10-22 20:22:46 +08:00
Tamim Hossain
77042f6fae Remove unnecessary null check from keywordFilter (#3737)
* Remove unnecessary null check from keywordFilter

- Replaced `keywordFilter.isNullOrEmpty()` with `keywordFilter.isEmpty()` since `keywordFilter` is guaranteed to never be null.
- Simplified the logic by removing the redundant null check, improving code readability.

* Remove unnecessary null check from keywordFilter

- Replaced `keywordFilter.isNullOrEmpty()` with `keywordFilter.isEmpty()` since `keywordFilter` is guaranteed to never be null.
- Simplified the logic by removing the redundant null check, improving code readability.
2024-10-22 20:22:23 +08:00
Tamim Hossain
013ac308f7 Refactor allowInsecure variable declaration (#3736)
- Changed `allowInsecure` from `var` to `val` to ensure immutability.
- Replaced the nullable safe call `settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false` with `settingsStorage.decodeBool(AppConfig.PREF_ALLOW_INSECURE, false)` for more concise and readable code.
- This ensures `allowInsecure` is always initialized with a default value of `false` in a cleaner and more efficient way.
2024-10-22 20:21:57 +08:00
any116
d703582f19 refine upload-artifact and fix version (#3735)
* refine upload-artifact and fix api version

* refine upload-artifact and fix api version

* refine upload-artifact and fix version

* use Dependabot keep actions updated to latest

like: v2-->v3
2024-10-22 20:21:16 +08:00
any116
1db80f740d Update SettingsManager.kt (#3732) 2024-10-22 09:18:13 +08:00
DecorativeFamily
a0d2740280 Update build.yml (#3731) 2024-10-22 09:17:59 +08:00
DecorativeFamily
297083f3c4 Update build.yml (#3730) 2024-10-22 09:17:39 +08:00
DecorativeFamily
15a4ad978a Update libs.versions.toml (#3728)
Co-authored-by: DECORATIVEFAMILYNG <185765765+DECORATIVEFAMILYNG@users.noreply.github.com>
2024-10-22 09:17:15 +08:00
DECORATIVEFAMILYNG
63f4cfac83 Update strings.xml (#3726)
update persian strings
2024-10-21 17:39:52 +08:00
Tamim Hossain
ca849fb19e Organize routing files names (#3717)
* Organize routing files names

Organize routing files names

* Use enum for routing type

Use enum for routing type
2024-10-21 17:39:28 +08:00
Tamim Hossain
3de3070ab7 Organize locale (#3716)
* Organize locale

Organize locale

* use enum for locale

use enum for locale
2024-10-21 17:38:41 +08:00
Tamim Hossain
cbea4bab7c Update kotlin version to 2.0.21 (#3724)
Update kotlin version to 2.0.21
2024-10-21 16:32:19 +08:00
Tamim Hossain
fe8b825c34 Fix subs id check (#3714)
* Fix subs id check

Fix subs id check

* Update MainViewModel.kt
2024-10-21 14:43:48 +08:00
2dust
daa0394960 Fix
https://github.com/2dust/v2rayNG/issues/3720
2024-10-21 14:42:00 +08:00
2dust
e5aba5d99b Adding checks for subscription url 2024-10-21 14:25:05 +08:00
any116
c4847eb3de Update SimpleItemTouchHelperCallback.java (#3719) 2024-10-21 14:04:24 +08:00
Tamim Hossain
5a5f911453 Use entries instead of values as its recommended after kotlin 1.9 (#3718)
Use entries instead of values as its  recommended after kotlin 1.9
2024-10-21 14:01:44 +08:00
Tamim Hossain
22cef29c27 organize dns (#3715)
organize dns
2024-10-21 13:55:45 +08:00
Tamim Hossain
ef41641680 Update bangla translation (#3713)
Update bangla translation
2024-10-21 13:52:11 +08:00
Tamim Hossain
69135e8707 Update plugins in ``libs.versions.toml`` (#3712)
Update plugins in ```libs.versions.toml```
2024-10-21 13:51:59 +08:00
2dust
5daef71147 up 1.9.8 2024-10-17 10:59:50 +08:00
Helium-Studio
5ffc5ec502 Clean up custom routing white (#3699)
Some IPs or domains are already included in geoip / geosite
2024-10-17 10:23:27 +08:00
2dust
35063db3e6 Improvement share uri 2024-10-17 10:21:13 +08:00
MMR
868c24bb8b Add Iran whitelist routing option (#3696)
* Add Iran whitelist routing option

* Update SettingsManager.kt

* Add files via upload

* Update custom_routing_white_iran

* Update strings.xml

* Update strings.xml

* Update strings.xml
2024-10-15 21:04:04 +08:00
2dust
7367baffb8 up 1.9.7 2024-10-09 17:39:16 +08:00
mayampi01
819ff2995a Add dns.pub to custom_routing_white (#3668) 2024-10-08 14:50:42 +08:00
2dust
3b5d04b717 Bug fix
https://github.com/2dust/v2rayNG/issues/3653
2024-10-08 10:26:22 +08:00
2dust
b673cd73ac Fix Violation of Broken Functionality policy 2024-10-08 10:25:39 +08:00
MH
649c1a022b Update strings.xml (#3652)
update persian strings
2024-10-05 17:52:41 +08:00
MH
034e58bc9d Update strings.xml (#3649) 2024-10-05 09:59:13 +08:00
2dust
a95f280102 up 1.9.6 2024-10-02 11:41:11 +08:00
2dust
df8da05f32 Fix routing rules 2024-10-02 11:13:41 +08:00
2dust
635581719b Fix latency test 2024-10-01 11:30:55 +08:00
NagisaEfi
77d5e203e8 Update strings.xml (#3639) 2024-10-01 09:50:51 +08:00
solokot
370d002b25 Update Russian translation (#3636) 2024-10-01 09:49:38 +08:00
2dust
18f0fe47ff up 1.9.5 2024-09-30 19:22:25 +08:00
2dust
cccd6139fc Adding latency test for hy2 2024-09-30 18:06:36 +08:00
2dust
1fadca8524 Add JsonUtil 2024-09-30 14:55:51 +08:00
2dust
af01e2ac06 Improvement Intent.serializable 2024-09-30 14:20:45 +08:00
2dust
de22e16cd4 Adjusting the default listening port for hy2 2024-09-30 13:31:10 +08:00
2dust
e073b19343 Update subscriptions through the proxy first, then update them directly
https://github.com/2dust/v2rayNG/issues/3627
2024-09-30 10:26:39 +08:00
2dust
a81a05cd45 Bug fix 2024-09-29 19:57:22 +08:00
2dust
fc67281d2a Bug fix
https://github.com/2dust/v2rayNG/issues/3625
2024-09-29 17:31:15 +08:00
2dust
ece35b9a1c Import and export ruleset via clipboard
https://github.com/2dust/v2rayCustomRoutingList/blob/master/custom_routing_rules_blacklist
https://github.com/2dust/v2rayCustomRoutingList/blob/master/custom_routing_rules_whitelist
2024-09-29 15:34:54 +08:00
2dust
ed8fb7fa82 Add obfs for Hysteria2 2024-09-29 11:07:45 +08:00
2dust
3688dd4634 Hy2 Protocol Share 2024-09-28 19:43:28 +08:00
solokot
7ff445ef55 Update Russian translation (#3616) 2024-09-28 19:38:40 +08:00
70 changed files with 1254 additions and 860 deletions

View File

@@ -26,18 +26,16 @@ jobs:
- name: Setup Golang
uses: actions/setup-go@v5
with:
go-version: '1.22.2'
go-version: '1.22.4'
- name: Install gomobile
run: |
go install golang.org/x/mobile/cmd/gomobile@latest
go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20240806205939-81131f6468ab
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Setup Android environment
uses: android-actions/setup-android@v3
- name: Build dependencies
run: |
mkdir ${{ github.workspace }}/build
@@ -56,8 +54,33 @@ jobs:
chmod 755 gradlew
./gradlew assembleDebug
- name: Upload APK
- name: Upload arm64-v8a APK
uses: actions/upload-artifact@v4
if: ${{ success() }}
with:
name: arm64-v8a
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*arm64-v8a*.apk
- name: Upload armeabi-v7a APK
uses: actions/upload-artifact@v4
if: ${{ success() }}
with:
name: armeabi-v7a
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*armeabi-v7a*.apk
- name: Upload x86 APK
uses: actions/upload-artifact@v4
if: ${{ success() }}
with:
name: x86-apk
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*x86*.apk
- name: Upload Other APKs
uses: actions/upload-artifact@v4
with:
name: apk
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/
name: others-apk
path: |
${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*arm64-v8a*.apk
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*armeabi-v7a*.apk
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*x86*.apk

View File

@@ -3,7 +3,7 @@
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-1.9.23-blue.svg)](https://kotlinlang.org)
[![Kotlin Version](https://img.shields.io/badge/Kotlin-2.0.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)

View File

@@ -11,8 +11,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 21
targetSdk = 34
versionCode = 597
versionName = "1.9.4"
versionCode = 603
versionName = "1.9.9"
multiDexEnabled = true
splits {
abi {

View File

@@ -27,13 +27,6 @@
"geosite:category-ads-all"
]
},
{
"remarks": "绕过局域网域名",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",
@@ -42,15 +35,14 @@
]
},
{
"remarks": "代理GFW",
"outboundTag": "proxy",
"remarks": "绕过局域网域名",
"outboundTag": "direct",
"domain": [
"geosite:gfw",
"geosite:greatfire"
"geosite:private"
]
},
{
"remarks": "代理Google等",
"remarks": "代理IP",
"outboundTag": "proxy",
"ip": [
"1.0.0.1",
@@ -65,6 +57,14 @@
"geoip:twitter"
]
},
{
"remarks": "代理GFW",
"outboundTag": "proxy",
"domain": [
"geosite:gfw",
"geosite:greatfire"
]
},
{
"remarks": "最终直连",
"port": "0-65535",

View File

@@ -12,13 +12,6 @@
"geosite:category-ads-all"
]
},
{
"remarks": "绕过局域网域名",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",
@@ -26,6 +19,13 @@
"geoip:private"
]
},
{
"remarks": "绕过局域网域名",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "最终代理",
"port": "0-65535",

View File

@@ -20,13 +20,6 @@
"geosite:category-ads-all"
]
},
{
"remarks": "绕过局域网域名",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",
@@ -35,16 +28,10 @@
]
},
{
"remarks": "绕过中国域名",
"remarks": "绕过局域网域名",
"outboundTag": "direct",
"domain": [
"domain:dns.alidns.com",
"domain:doh.pub",
"domain:dot.pub",
"domain:doh.360.cn",
"domain:dot.360.cn",
"geosite:cn",
"geosite:geolocation-cn"
"geosite:private"
]
},
{
@@ -73,6 +60,19 @@
"geoip:cn"
]
},
{
"remarks": "绕过中国域名",
"outboundTag": "direct",
"domain": [
"domain:dns.alidns.com",
"domain:doh.pub",
"domain:dot.pub",
"domain:doh.360.cn",
"domain:dot.360.cn",
"geosite:cn",
"geosite:geolocation-cn"
]
},
{
"remarks": "最终代理",
"port": "0-65535",

View File

@@ -0,0 +1,49 @@
[
{
"remarks": "Block udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "Block ads and trackers",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "Direct LAN IP",
"outboundTag": "direct",
"ip": [
"geoip:private"
]
},
{
"remarks": "Direct LAN domains",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "Bypass Iran domains",
"outboundTag": "direct",
"domain": [
"domain:ir",
"geosite:category-ir"
]
},
{
"remarks": "Bypass Iran IP",
"outboundTag": "direct",
"ip": [
"geoip:ir"
]
},
{
"remarks": "Final Agent",
"port": "0-65535",
"outboundTag": "proxy"
}
]

View File

@@ -16,14 +16,14 @@
package com.v2ray.ang.helper;
import android.animation.ValueAnimator;
import android.graphics.Canvas;
import android.view.animation.DecelerateInterpolator;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
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.<br/>
@@ -36,9 +36,12 @@ import org.jetbrains.annotations.NotNull;
*/
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
public static final float ALPHA_FULL = 1.0f;
private static final float ALPHA_FULL = 1.0f;
private static final float SWIPE_THRESHOLD = 0.25f;
private static final long ANIMATION_DURATION = 200;
private final ItemTouchHelperAdapter mAdapter;
private ValueAnimator mReturnAnimator;
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
mAdapter = adapter;
@@ -51,15 +54,14 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
@Override
public boolean isItemViewSwipeEnabled() {
return false;
return true;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
// Set movement flags based on the layout manager
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
final int swipeFlags = 0;
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
} else {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
@@ -69,61 +71,89 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
}
@Override
public boolean onMove(@NotNull RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder source, @NonNull 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());
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
// 不执行删除操作,仅返回项目到原位
returnViewToOriginalPosition(viewHolder);
}
@Override
public void onChildDraw(@NotNull Canvas c, @NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder, float dX,
float dY, int actionState, boolean isCurrentlyActive) {
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
// 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();
float maxSwipeDistance = viewHolder.itemView.getWidth() * SWIPE_THRESHOLD;
float swipeAmount = Math.abs(dX);
float direction = Math.signum(dX);
// 限制最大滑动距离
float translationX = Math.min(swipeAmount, maxSwipeDistance) * direction;
float alpha = ALPHA_FULL - Math.min(swipeAmount, maxSwipeDistance) / maxSwipeDistance;
viewHolder.itemView.setTranslationX(translationX);
viewHolder.itemView.setAlpha(alpha);
viewHolder.itemView.setTranslationX(dX);
if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
returnViewToOriginalPosition(viewHolder);
}
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
}
private void returnViewToOriginalPosition(RecyclerView.ViewHolder viewHolder) {
if (mReturnAnimator != null && mReturnAnimator.isRunning()) {
mReturnAnimator.cancel();
}
mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.getTranslationX(), 0f);
mReturnAnimator.addUpdateListener(animation -> {
float value = (float) animation.getAnimatedValue();
viewHolder.itemView.setTranslationX(value);
viewHolder.itemView.setAlpha(1f - Math.abs(value) / (viewHolder.itemView.getWidth() * SWIPE_THRESHOLD));
});
mReturnAnimator.setInterpolator(new DecelerateInterpolator());
mReturnAnimator.setDuration(ANIMATION_DURATION);
mReturnAnimator.start();
}
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
// 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) {
public void clearView(@NonNull RecyclerView recyclerView, @NonNull 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();
}
mAdapter.onItemMoveCompleted();
}
@Override
public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
return 1.1f; // 设置一个大于1的值确保不会触发默认的滑动删除操作
}
@Override
public float getSwipeEscapeVelocity(float defaultValue) {
return defaultValue * 10; // 增加滑动逃逸速度,使得更难触发滑动
}
}

View File

@@ -60,8 +60,6 @@ object AppConfig {
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
/** Protocol identifiers. */
const val PROTOCOL_HTTP: String = "http://"
const val PROTOCOL_HTTPS: String = "https://"
const val PROTOCOL_FREEDOM: String = "freedom"
/** Broadcast actions. */
@@ -105,7 +103,10 @@ object AppConfig {
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"
@@ -150,8 +151,25 @@ object AppConfig {
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_PUB_DOMAIN = "dns.pub"
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
const val DNS_ONE_ONE_DOMAIN = "one.one.one.one"
const val DNS_GOOGLE_DOMAIN = "dns.google"
val DNS_PUB_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
val DNS_ONE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
}

View File

@@ -0,0 +1,9 @@
package com.v2ray.ang.dto
data class ConfigResult (
var status: Boolean,
var guid: String? = null,
var content: String = "",
var domainPort: String? = null,
)

View File

@@ -16,6 +16,6 @@ enum class EConfigType(val value: Int, val protocolScheme: String) {
HTTP(10, AppConfig.HTTP);
companion object {
fun fromInt(value: Int) = values().firstOrNull { it.value == value }
fun fromInt(value: Int) = entries.firstOrNull { it.value == value }
}
}

View File

@@ -4,9 +4,20 @@ data class Hysteria2Bean(
val server: String?,
val auth: String?,
val lazy: Boolean? = true,
val socks5: Socks5Bean?,
val tls: TlsBean?
val obfs: ObfsBean? = null,
val socks5: Socks5Bean? = null,
val http: Socks5Bean? = null,
val tls: TlsBean? = null,
) {
data class ObfsBean(
val type: String?,
val salamander: SalamanderBean?
) {
data class SalamanderBean(
val password: String?,
)
}
data class Socks5Bean(
val listen: String?,
)
@@ -15,4 +26,4 @@ data class Hysteria2Bean(
val sni: String?,
val insecure: Boolean?,
)
}
}

View File

@@ -0,0 +1,18 @@
package com.v2ray.ang.dto
enum class Language(val code: String) {
AUTO("auto"),
ENGLISH("en"),
CHINA("zh-rCN"),
TRADITIONAL_CHINESE("zh-rTW"),
VIETNAMESE("vi"),
RUSSIAN("ru"),
PERSIAN("fa"),
BANGLA("bn");
companion object {
fun fromCode(code: String): Language {
return entries.find { it.code == code } ?: AUTO
}
}
}

View File

@@ -0,0 +1,20 @@
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
}
}
}
}

View File

@@ -104,7 +104,8 @@ data class V2rayConfig(
var secretKey: String? = null,
val peers: List<WireGuardBean>? = null,
var reserved: List<Int>? = null,
var mtu: Int? = null
var mtu: Int? = null,
var obfsPassword: String? = null,
) {
data class VnextBean(
@@ -320,8 +321,8 @@ data class V2rayConfig(
tcpSetting.header.type = HTTP
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
requestObj.headers.Host = (host.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() }
requestObj.path = (path.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() }
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
tcpSetting.header.request = requestObj
sni = requestObj.headers.Host?.getOrNull(0) ?: sni
}
@@ -370,7 +371,7 @@ data class V2rayConfig(
"h2", "http" -> {
network = "h2"
val h2Setting = HttpSettingsBean()
h2Setting.host = (host.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() }
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
sni = h2Setting.host.getOrNull(0) ?: sni
h2Setting.path = path ?: "/"
httpSettings = h2Setting

View File

@@ -5,10 +5,12 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import com.v2ray.ang.AngApplication
import me.drakeet.support.toast.ToastCompat
import org.json.JSONObject
import java.io.Serializable
import java.net.URI
import java.net.URLConnection
@@ -81,4 +83,14 @@ fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Uni
addDataScheme("package")
})
}
}
}
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializable(key) as? T
}
inline fun <reified T : Serializable> 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
}

View File

@@ -0,0 +1,45 @@
package com.v2ray.ang.service
import android.content.Context
import android.util.Log
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ProcessService {
private val TAG = ANG_PACKAGE
private lateinit var process: Process
fun runProcess(context: Context, cmd: MutableList<String>) {
Log.d(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.d(TAG, "runProcess check")
process.waitFor()
Log.d(TAG, "runProcess exited")
}
Log.d(TAG, process.toString())
} catch (e: Exception) {
Log.d(TAG, e.toString())
}
}
fun stopProcess() {
try {
Log.d(TAG, "runProcess destroy")
process?.destroy()
} catch (e: Exception) {
Log.d(TAG, e.toString())
}
}
}

View File

@@ -15,6 +15,7 @@ import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME
import com.v2ray.ang.R
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
@@ -40,8 +41,8 @@ object SubscriptionUpdater {
val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate }
for (i in subs) {
val subscription = i.second
for (sub in subs) {
val subItem = sub.second
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notification.setChannelId(SUBSCRIPTION_UPDATE_CHANNEL)
@@ -56,11 +57,10 @@ object SubscriptionUpdater {
notificationManager.notify(3, notification.build())
Log.d(
AppConfig.ANG_PACKAGE,
"subscription automatic update: ---${subscription.remarks}"
"subscription automatic update: ---${subItem.remarks}"
)
val configs = Utils.getUrlContentWithCustomUserAgent(subscription.url)
AngConfigManager.importBatchConfig(configs, i.first, false)
notification.setContentText("Updating ${subscription.remarks}")
updateConfigViaSub(Pair(sub.first, subItem))
notification.setContentText("Updating ${subItem.remarks}")
}
notificationManager.cancel(3)
return Result.success()

View File

@@ -164,7 +164,7 @@ object V2RayServiceManager {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
showNotification()
PluginUtil.runPlugin(service, config)
PluginUtil.runPlugin(service, config, result.domainPort)
} else {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
cancelNotification()
@@ -428,10 +428,11 @@ object V2RayServiceManager {
}
private fun stopSpeedNotification() {
if (mDisposable != null) {
mDisposable?.dispose() //stop queryStats
mDisposable?.let {
it.dispose() //stop queryStats
mDisposable = null
updateNotification(currentConfig?.remarks, 0, 0)
}
}
}

View File

@@ -6,9 +6,14 @@ 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.util.MessageUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.PluginUtil
import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.V2rayConfigUtil
import go.Seq
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -30,10 +35,10 @@ class V2RayTestService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.getIntExtra("key", 0)) {
MSG_MEASURE_CONFIG -> {
val contentPair = intent.getSerializableExtra("content") as Pair<String, String>
val guid = intent.serializable<String>("content") ?: ""
realTestScope.launch {
val result = SpeedtestUtil.realPing(contentPair.second)
MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(contentPair.first, result))
val result = startRealPing(guid)
MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result))
}
}
@@ -47,4 +52,20 @@ class V2RayTestService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
private fun startRealPing(guid: String): Long {
val retFailure = -1L
val server = MmkvManager.decodeServerConfig(guid) ?: return retFailure
if (server.getProxyOutbound()?.protocol?.equals(EConfigType.HYSTERIA2.name, true) == true) {
val delay = PluginUtil.realPingHy2(this, server)
return delay
} else {
val config = V2rayConfigUtil.getV2rayConfig(this, guid)
if (!config.status) {
return retFailure
}
return SpeedtestUtil.realPing(config.content)
}
}
}

View File

@@ -203,7 +203,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
}
private fun runTun2socks() {
val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
val socksPort = SettingsManager.getSocksPort()
val cmd = arrayListOf(
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,

View File

@@ -4,6 +4,7 @@ import android.Manifest
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import com.tbruyelle.rxpermissions3.RxPermissions
@@ -133,13 +134,15 @@ class AboutActivity : BaseActivity() {
}
private fun showFileChooser() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
type = "*/*"
addCategory(Intent.CATEGORY_OPENABLE)
}
try {
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
} catch (ex: android.content.ActivityNotFoundException) {
Log.e(AppConfig.ANG_PACKAGE, "File chooser activity not found: ${ex.message}", ex)
toast(R.string.toast_require_file_manager)
}
}
@@ -149,27 +152,23 @@ class AboutActivity : BaseActivity() {
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
try {
try {
val targetFile =
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
contentResolver.openInputStream(uri).use { input ->
targetFile.outputStream().use { fileOut ->
input?.copyTo(fileOut)
}
val targetFile =
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
contentResolver.openInputStream(uri).use { input ->
targetFile.outputStream().use { fileOut ->
input?.copyTo(fileOut)
}
if (restoreConfiguration(targetFile)) {
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
} catch (e: Exception) {
e.printStackTrace()
}
if (restoreConfiguration(targetFile)) {
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
} catch (e: Exception) {
e.printStackTrace()
toast(e.message.toString())
Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e)
toast(R.string.toast_failure)
}
}
}
}

View File

@@ -34,9 +34,6 @@ abstract class BaseActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.N)
override fun attachBaseContext(newBase: Context?) {
val context = newBase?.let {
MyContextWrapper.wrap(newBase, Utils.getLocale())
}
super.attachBaseContext(context)
super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, Utils.getLocale()))
}
}

View File

@@ -33,46 +33,42 @@ class LogcatActivity : BaseActivity() {
}
private fun logcat(shouldFlushLog: Boolean) {
binding.pbWaiting.visibility = View.VISIBLE
try {
binding.pbWaiting.visibility = View.VISIBLE
lifecycleScope.launch(Dispatchers.Default) {
lifecycleScope.launch(Dispatchers.Default) {
try {
if (shouldFlushLog) {
val lst = LinkedHashSet<String>()
lst.add("logcat")
lst.add("-c")
val lst = linkedSetOf("logcat", "-c")
withContext(Dispatchers.IO) {
val process = Runtime.getRuntime().exec(lst.toTypedArray())
process.waitFor()
}
}
val lst = LinkedHashSet<String>()
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 lst = linkedSetOf(
"logcat", "-d", "-v", "time", "-s",
"GoLog,tun2socks,$ANG_PACKAGE,AndroidRuntime,System.err"
)
val process = withContext(Dispatchers.IO) {
Runtime.getRuntime().exec(lst.toTypedArray())
}
// val bufferedReader = BufferedReader(
// InputStreamReader(process.inputStream))
// val allText = bufferedReader.use(BufferedReader::readText)
val allText = process.inputStream.bufferedReader().use { it.readText() }
launch(Dispatchers.Main) {
withContext(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) {
withContext(Dispatchers.Main) {
binding.pbWaiting.visibility = View.GONE
toast(R.string.toast_failure)
}
e.printStackTrace()
}
} catch (e: IOException) {
e.printStackTrace()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_logcat, menu)
return super.onCreateOptionsMenu(menu)

View File

@@ -46,6 +46,7 @@ import io.reactivex.rxjava3.core.Observable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.drakeet.support.toast.ToastCompat
import java.util.concurrent.TimeUnit
@@ -199,6 +200,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
fun startV2Ray() {
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
toast(R.string.title_file_chooser)
return
}
V2RayServiceManager.startV2Ray(this)
@@ -340,11 +342,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
R.id.ping_all -> {
toast(R.string.connection_test_testing)
mainViewModel.testAllTcping()
true
}
R.id.real_ping_all -> {
toast(R.string.connection_test_testing)
mainViewModel.testAllRealPing()
true
}
@@ -488,30 +492,34 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
private fun importBatchConfig(server: String?) {
// val dialog = AlertDialog.Builder(this)
// .setView(LayoutProgressBinding.inflate(layoutInflater).root)
// .setCancelable(false)
// .show()
binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
val (count, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true)
delay(500L)
launch(Dispatchers.Main) {
if (count > 0) {
toast(R.string.toast_success)
mainViewModel.reloadServerList()
} else if (countSub > 0) {
initGroupTab()
} else {
toast(R.string.toast_failure)
try {
val (count, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true)
delay(500L)
withContext(Dispatchers.Main) {
when {
count > 0 -> {
toast(R.string.toast_success)
mainViewModel.reloadServerList()
}
countSub > 0 -> initGroupTab()
else -> toast(R.string.toast_failure)
}
binding.pbWaiting.hide()
}
//dialog.dismiss()
binding.pbWaiting.hide()
} catch (e: Exception) {
withContext(Dispatchers.Main) {
toast(R.string.toast_failure)
binding.pbWaiting.hide()
}
e.printStackTrace()
}
}
}
private fun importConfigCustomClipboard()
: Boolean {
try {
@@ -598,8 +606,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
delay(500L)
launch(Dispatchers.Main) {
if (count > 0) {
toast(R.string.toast_success)
//toast(R.string.toast_success)
toast(R.string.connection_test_testing)
mainViewModel.reloadServerList()
mainViewModel.testAllRealPing()
} else {
toast(R.string.toast_failure)
}

View File

@@ -59,22 +59,23 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
fun bind(appInfo: AppInfo) {
this.appInfo = appInfo
// Set app icon and name
itemBypassBinding.icon.setImageDrawable(appInfo.appIcon)
// name.text = appInfo.appName
itemBypassBinding.checkBox.isChecked = inBlacklist
itemBypassBinding.packageName.text = appInfo.packageName
if (appInfo.isSystemApp) {
itemBypassBinding.name.text = String.format("** %1s", appInfo.appName)
//name.textColor = Color.RED
itemBypassBinding.name.text = if (appInfo.isSystemApp) {
String.format("** %s", appInfo.appName)
} else {
itemBypassBinding.name.text = appInfo.appName
//name.textColor = Color.DKGRAY
appInfo.appName
}
// Set package name and checkbox state
itemBypassBinding.packageName.text = appInfo.packageName
itemBypassBinding.checkBox.isChecked = inBlacklist
// Handle item click to toggle blacklist status
itemView.setOnClickListener(this)
}
override fun onClick(v: View?) {
if (inBlacklist) {
blacklist.remove(appInfo.packageName)

View File

@@ -59,16 +59,21 @@ class RoutingEditActivity : BaseActivity() {
private fun saveServer(): Boolean {
val rulesetItem = SettingsManager.getRoutingRuleset(position) ?: RulesetItem()
rulesetItem.remarks = binding.etRemarks.text.toString()
rulesetItem.looked = binding.chkLocked.isChecked
binding.etDomain.text.toString().let { rulesetItem.domain = if (it.isEmpty()) null else it.split(",").map { itt -> itt.trim() }.filter { itt -> itt.isNotEmpty() } }
binding.etIp.text.toString().let { rulesetItem.ip = if (it.isEmpty()) null else it.split(",").map { itt -> itt.trim() }.filter { itt -> itt.isNotEmpty() } }
binding.etProtocol.text.toString().let { rulesetItem.protocol = if (it.isEmpty()) null else it.split(",").map { itt -> itt.trim() }.filter { itt -> itt.isNotEmpty() } }
binding.etPort.text.toString().let { rulesetItem.port = it.ifEmpty { null } }
binding.etNetwork.text.toString().let { rulesetItem.network = it.ifEmpty { null } }
rulesetItem.outboundTag = outbound_tag[binding.spOutboundTag.selectedItemPosition]
rulesetItem.apply {
remarks = binding.etRemarks.text.toString()
looked = 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 (TextUtils.isEmpty(rulesetItem.remarks)) {
if (rulesetItem.remarks.isNullOrEmpty()) {
toast(R.string.sub_setting_remarks)
return false
}
@@ -79,6 +84,7 @@ class RoutingEditActivity : BaseActivity() {
return true
}
private fun deleteServer(): Boolean {
if (position >= 0) {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)

View File

@@ -10,18 +10,21 @@ 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.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.SettingsManager
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) }
@@ -108,11 +111,54 @@ class RoutingSettingActivity : BaseActivity() {
true
}
R.id.import_rulesets_from_clipboard -> {
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
.setPositiveButton(android.R.string.ok) { _, _ ->
val clipboard = try {
Utils.getClipboard(this)
} catch (e: Exception) {
e.printStackTrace()
toast(R.string.toast_failure)
return@setPositiveButton
}
lifecycleScope.launch(Dispatchers.IO) {
val result = SettingsManager.resetRoutingRulesetsFromClipboard(clipboard)
withContext(Dispatchers.Main) {
if (result) {
refreshData()
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
}
}
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do nothing
}
.show()
true
}
R.id.export_rulesets_to_clipboard -> {
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) {
toast(R.string.toast_failure)
} else {
Utils.setClipboard(this, JsonUtil.toJson(rulesetList))
toast(R.string.toast_success)
}
true
}
else -> super.onOptionsItemSelected(item)
}
fun refreshData() {
rulesets = MmkvManager.decodeRoutingRulesets() ?: mutableListOf()
rulesets.clear()
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
adapter.notifyDataSetChanged()
}
}

View File

@@ -36,7 +36,8 @@ class RoutingSettingRecyclerAdapter(val activity: RoutingSettingActivity) : Recy
)
}
holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { _, isChecked ->
holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
if( !it.isPressed) return@setOnCheckedChangeListener
ruleset.enabled = isChecked
SettingsManager.saveRoutingRuleset(position, ruleset)
}

View File

@@ -20,26 +20,31 @@ class ScScannerActivity : BaseActivity() {
fun importQRcode(): Boolean {
RxPermissions(this)
.request(Manifest.permission.CAMERA)
.subscribe {
if (it)
.subscribe { granted ->
if (granted) {
scanQRCode.launch(Intent(this, ScannerActivity::class.java))
else
} else {
toast(R.string.toast_permission_denied)
}
}
return true
}
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
val (count, countSub) = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false)
val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty()
val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false)
if (count + countSub > 0) {
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
startActivity(Intent(this, MainActivity::class.java))
}
finish()
}
}

View File

@@ -74,19 +74,17 @@ class ScannerActivity : BaseActivity() {
}
RxPermissions(this)
.request(permission)
.subscribe {
if (it) {
try {
showFileChooser()
} catch (e: Exception) {
e.printStackTrace()
}
} else
.subscribe { granted ->
if (granted) {
showFileChooser()
} else {
toast(R.string.toast_permission_denied)
}
}
true
}
else -> super.onOptionsItemSelected(item)
}
@@ -107,13 +105,21 @@ class ScannerActivity : BaseActivity() {
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
try {
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
val inputStream = contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
finished(text.orEmpty())
if (text.isNullOrEmpty()) {
toast(R.string.toast_decoding_failed)
} else {
finished(text)
}
} catch (e: Exception) {
e.printStackTrace()
toast(e.message.toString())
toast(R.string.toast_decoding_failed)
}
}
}
}

View File

@@ -118,6 +118,7 @@ class ServerActivity : BaseActivity() {
private val et_reserved3: EditText? by lazy { findViewById(R.id.et_reserved3) }
private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) }
private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) }
private val et_obfs_password: EditText? by lazy { findViewById(R.id.et_obfs_password) }
override fun onCreate(savedInstanceState: Bundle?) {
@@ -200,31 +201,49 @@ class ServerActivity : BaseActivity() {
position: Int,
id: Long
) {
if (streamSecuritys[position].isBlank()) {
container_sni?.visibility = View.GONE
container_fingerprint?.visibility = View.GONE
container_alpn?.visibility = View.GONE
container_allow_insecure?.visibility = View.GONE
container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE
} else {
container_sni?.visibility = View.VISIBLE
container_fingerprint?.visibility = View.VISIBLE
container_alpn?.visibility = View.VISIBLE
if (streamSecuritys[position] == TLS) {
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
container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE
} else {
container_allow_insecure?.visibility = View.GONE
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_public_key?.visibility = View.VISIBLE
container_short_id?.visibility = View.VISIBLE
container_spider_x?.visibility = View.VISIBLE
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<*>?) {
@@ -292,7 +311,10 @@ class ServerActivity : BaseActivity() {
} else {
et_local_mtu?.text = Utils.getEditable(outbound.settings?.mtu.toString())
}
} else if (config.configType == EConfigType.HYSTERIA2) {
et_obfs_password?.text = Utils.getEditable(outbound.settings?.obfsPassword)
}
val securityEncryptions =
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
val security =
@@ -455,6 +477,9 @@ class ServerActivity : BaseActivity() {
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
config.subscriptionId = subscriptionId.orEmpty()
}
if (config.configType == EConfigType.HYSTERIA2) {
config.outboundBean?.settings?.obfsPassword = et_obfs_password?.text?.toString()
}
MmkvManager.encodeServerConfig(editGuid, config)
toast(R.string.toast_success)
@@ -553,11 +578,12 @@ class ServerActivity : BaseActivity() {
val shortId = et_short_id?.text?.toString()
val spiderX = et_spider_x?.text?.toString()
val allowInsecure = if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false
} else {
allowinsecures[allowInsecureField].toBoolean()
}
val allowInsecure =
if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false
} else {
allowinsecures[allowInsecureField].toBoolean()
}
streamSetting.populateTlsSettings(
streamSecurity = streamSecuritys[streamSecurity],

View File

@@ -8,13 +8,14 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.blacksquircle.ui.editorkit.utils.EditorTheme
import com.blacksquircle.ui.language.json.JsonLanguage
import com.google.gson.Gson
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import me.drakeet.support.toast.ToastCompat
@@ -47,16 +48,14 @@ class ServerCustomConfigActivity : BaseActivity() {
}
/**
* bingding seleced server config
* Binding selected server config
*/
private fun bindingServer(config: ServerConfig): Boolean {
binding.etRemarks.text = Utils.getEditable(config.remarks)
val raw = MmkvManager.decodeServerRaw(editGuid)
if (raw.isNullOrBlank()) {
binding.editor.setTextContent(Utils.getEditable(config.fullConfig?.toPrettyPrinting().orEmpty()))
} else {
binding.editor.setTextContent(Utils.getEditable(raw))
}
val configContent = raw ?: config.fullConfig?.toPrettyPrinting().orEmpty()
binding.editor.setTextContent(Utils.getEditable(configContent))
return true
}
@@ -78,7 +77,7 @@ class ServerCustomConfigActivity : BaseActivity() {
}
val v2rayConfig = try {
Gson().fromJson(binding.editor.text.toString(), V2rayConfig::class.java)
JsonUtil.fromJson(binding.editor.text.toString(), V2rayConfig::class.java)
} catch (e: Exception) {
e.printStackTrace()
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()

View File

@@ -316,13 +316,11 @@ class SettingsActivity : BaseActivity() {
}
private fun updateMuxConcurrency(value: String?) {
if (value == null) {
} else {
val concurrency = value.toIntOrNull() ?: 8
muxConcurrency?.summary = concurrency.toString()
}
val concurrency = value?.toIntOrNull() ?: 8
muxConcurrency?.summary = concurrency.toString()
}
private fun updateMuxXudpConcurrency(value: String?) {
if (value == null) {
muxXudpQuic?.isEnabled = true

View File

@@ -81,10 +81,17 @@ class SubEditActivity : BaseActivity() {
toast(R.string.sub_setting_remarks)
return false
}
// if (TextUtils.isEmpty(subItem.url)) {
// toast(R.string.sub_setting_url)
// return false
// }
if (subItem.url.isNotEmpty()) {
if (!Utils.isValidUrl(subItem.url)) {
toast(R.string.toast_invalid_url)
return false
}
if (!Utils.isValidSubUrl(subItem.url)) {
toast(R.string.toast_insecure_url_protocol)
//return false
}
}
MmkvManager.encodeSubscription(editSubId, subItem)
toast(R.string.toast_success)

View File

@@ -44,7 +44,8 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
)
}
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { _, isChecked ->
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
if( !it.isPressed) return@setOnCheckedChangeListener
subItem.enabled = isChecked
MmkvManager.encodeSubscription(subId, subItem)

View File

@@ -31,6 +31,7 @@ import com.v2ray.ang.extension.toTrafficString
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.SettingsManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -70,23 +71,11 @@ class UserAssetActivity : BaseActivity() {
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.add_file -> {
showFileChooser()
true
}
R.id.add_url -> {
val intent = Intent(this, UserAssetUrlActivity::class.java)
startActivity(intent)
true
}
R.id.download_file -> {
downloadGeoFiles()
true
}
// Use when to streamline the option selection
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.add_file -> showFileChooser().let { true }
R.id.add_url -> startActivity(Intent(this, UserAssetUrlActivity::class.java)).let { true }
R.id.download_file -> downloadGeoFiles().let { true }
else -> super.onOptionsItemSelected(item)
}
@@ -119,31 +108,29 @@ class UserAssetActivity : BaseActivity() {
}
}
private val chooseFile =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { it ->
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
val assetId = Utils.getUuid()
try {
val assetItem = AssetUrlItem(
getCursorName(uri) ?: uri.toString(),
"file"
)
val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val uri = result.data?.data
if (result.resultCode == RESULT_OK && uri != null) {
val assetId = Utils.getUuid()
runCatching {
val assetItem = AssetUrlItem(
getCursorName(uri) ?: uri.toString(),
"file"
)
// check remarks unique
val assetList = MmkvManager.decodeAssetUrls()
if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
toast(R.string.msg_remark_is_duplicate)
return@registerForActivityResult
}
val assetList = MmkvManager.decodeAssetUrls()
if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
toast(R.string.msg_remark_is_duplicate)
} else {
MmkvManager.encodeAsset(assetId, assetItem)
copyFile(uri)
} catch (e: Exception) {
toast(R.string.toast_asset_copy_failed)
MmkvManager.removeAssetUrl(assetId)
}
}.onFailure {
toast(R.string.toast_asset_copy_failed)
MmkvManager.removeAssetUrl(assetId)
}
}
}
private fun copyFile(uri: Uri): String {
val targetFile = File(extDir, getCursorName(uri) ?: uri.toString())
@@ -176,7 +163,7 @@ class UserAssetActivity : BaseActivity() {
.show()
toast(R.string.msg_downloading_content)
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
val httpPort = SettingsManager.getHttpPort()
var assets = MmkvManager.decodeAssetUrls()
assets = addBuiltInGeoItems(assets)

View File

@@ -4,16 +4,16 @@ import android.content.Context
import android.graphics.Bitmap
import android.text.TextUtils
import android.util.Log
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.reflect.TypeToken
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.HY2
import com.v2ray.ang.R
import com.v2ray.ang.dto.*
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.fmt.Hysteria2Fmt
import com.v2ray.ang.util.fmt.ShadowsocksFmt
import com.v2ray.ang.util.fmt.SocksFmt
@@ -52,7 +52,7 @@ object AngConfigManager {
VlessFmt.parse(str)
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
WireguardFmt.parse(str)
} else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme)) {
} else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme) || str.startsWith(HY2)) {
Hysteria2Fmt.parse(str)
} else {
null
@@ -219,7 +219,7 @@ object AngConfigManager {
var count = 0
servers.lines()
.forEach { str ->
if (str.startsWith(AppConfig.PROTOCOL_HTTP) || str.startsWith(AppConfig.PROTOCOL_HTTPS)) {
if (Utils.isValidSubUrl(str)) {
count += importUrlAsSubscription(str)
}
}
@@ -278,34 +278,21 @@ object AngConfigManager {
&& server.contains("routing")
) {
try {
//val gson = GsonBuilder().setPrettyPrinting().create()
val gson = GsonBuilder()
.setPrettyPrinting()
.disableHtmlEscaping()
.registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
object : TypeToken<Double>() {}.type,
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? ->
JsonPrimitive(
src?.toInt()
)
}
)
.create()
val serverList: Array<Any> =
Gson().fromJson(server, Array<Any>::class.java)
JsonUtil.fromJson(server, Array<Any>::class.java)
if (serverList.isNotEmpty()) {
var count = 0
for (srv in serverList.reversed()) {
val config = ServerConfig.create(EConfigType.CUSTOM)
config.fullConfig =
Gson().fromJson(Gson().toJson(srv), V2rayConfig::class.java)
JsonUtil.fromJson(JsonUtil.toJson(srv), V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks
?: ("%04d-".format(count + 1) + System.currentTimeMillis()
.toString())
config.subscriptionId = subid
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, gson.toJson(srv))
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv))
count += 1
}
return count
@@ -318,7 +305,7 @@ object AngConfigManager {
// For compatibility
val config = ServerConfig.create(EConfigType.CUSTOM)
config.subscriptionId = subid
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
config.fullConfig = JsonUtil.fromJson(server, V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
@@ -373,19 +360,17 @@ object AngConfigManager {
return 0
}
Log.d(AppConfig.ANG_PACKAGE, url)
var configText = try {
Utils.getUrlContentWithCustomUserAgent(url)
val httpPort = SettingsManager.getHttpPort()
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
} catch (e: Exception) {
e.printStackTrace()
""
}
if (configText.isEmpty()) {
configText = try {
val httpPort = Utils.parseInt(
settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT),
AppConfig.PORT_HTTP.toInt()
)
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
Utils.getUrlContentWithCustomUserAgent(url)
} catch (e: Exception) {
e.printStackTrace()
""

View File

@@ -0,0 +1,37 @@
package com.v2ray.ang.util
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
object JsonUtil {
private var gson = Gson()
fun toJson(src: Any?): String {
return gson.toJson(src)
}
fun <T> fromJson(json: String, cls: Class<T>): T {
return gson.fromJson(json, cls)
}
fun toJsonPretty(src: Any?): String {
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<Double>() {}.type,
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? ->
JsonPrimitive(
src?.toInt()
)
}
)
.create()
return gsonPre.toJson(src)
}
}

View File

@@ -1,6 +1,6 @@
package com.v2ray.ang.util
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
@@ -49,7 +49,7 @@ object MmkvManager {
}
fun encodeServerList(serverList: MutableList<String>) {
mainStorage.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
}
fun decodeServerList(): MutableList<String> {
@@ -57,7 +57,7 @@ object MmkvManager {
return if (json.isNullOrBlank()) {
mutableListOf()
} else {
Gson().fromJson(json, Array<String>::class.java).toMutableList()
JsonUtil.fromJson(json, Array<String>::class.java).toMutableList()
}
}
@@ -69,7 +69,7 @@ object MmkvManager {
if (json.isNullOrBlank()) {
return null
}
return Gson().fromJson(json, ServerConfig::class.java)
return JsonUtil.fromJson(json, ServerConfig::class.java)
}
fun decodeProfileConfig(guid: String): ProfileItem? {
@@ -80,12 +80,12 @@ object MmkvManager {
if (json.isNullOrBlank()) {
return null
}
return Gson().fromJson(json, ProfileItem::class.java)
return JsonUtil.fromJson(json, ProfileItem::class.java)
}
fun encodeServerConfig(guid: String, config: ServerConfig): String {
val key = guid.ifBlank { Utils.getUuid() }
serverStorage.encode(key, Gson().toJson(config))
serverStorage.encode(key, JsonUtil.toJson(config))
val serverList = decodeServerList()
if (!serverList.contains(key)) {
serverList.add(0, key)
@@ -101,7 +101,7 @@ object MmkvManager {
server = config.getProxyOutbound()?.getServerAddress(),
serverPort = config.getProxyOutbound()?.getServerPort(),
)
profileStorage.encode(key, Gson().toJson(profile))
profileStorage.encode(key, JsonUtil.toJson(profile))
return key
}
@@ -141,7 +141,7 @@ object MmkvManager {
if (json.isNullOrBlank()) {
return null
}
return Gson().fromJson(json, ServerAffiliationInfo::class.java)
return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java)
}
fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
@@ -150,14 +150,14 @@ object MmkvManager {
}
val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo()
aff.testDelayMillis = testResult
serverAffStorage.encode(guid, Gson().toJson(aff))
serverAffStorage.encode(guid, JsonUtil.toJson(aff))
}
fun clearAllTestDelayResults(keys: List<String>?) {
keys?.forEach { key ->
decodeServerAffiliationInfo(key)?.let { aff ->
aff.testDelayMillis = 0
serverAffStorage.encode(key, Gson().toJson(aff))
serverAffStorage.encode(key, JsonUtil.toJson(aff))
}
}
}
@@ -217,7 +217,7 @@ object MmkvManager {
decodeSubsList().forEach { key ->
val json = subStorage.decodeString(key)
if (!json.isNullOrBlank()) {
subscriptions.add(Pair(key, Gson().fromJson(json, SubscriptionItem::class.java)))
subscriptions.add(Pair(key, JsonUtil.fromJson(json, SubscriptionItem::class.java)))
}
}
return subscriptions
@@ -234,7 +234,7 @@ object MmkvManager {
fun encodeSubscription(guid: String, subItem: SubscriptionItem) {
val key = guid.ifBlank { Utils.getUuid() }
subStorage.encode(key, Gson().toJson(subItem))
subStorage.encode(key, JsonUtil.toJson(subItem))
val subsList = decodeSubsList()
if (!subsList.contains(key)) {
@@ -245,11 +245,11 @@ object MmkvManager {
fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
val json = subStorage.decodeString(subscriptionId) ?: return null
return Gson().fromJson(json, SubscriptionItem::class.java)
return JsonUtil.fromJson(json, SubscriptionItem::class.java)
}
fun encodeSubsList(subsList: MutableList<String>) {
mainStorage.encode(KEY_SUB_IDS, Gson().toJson(subsList))
mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList))
}
fun decodeSubsList(): MutableList<String> {
@@ -257,7 +257,7 @@ object MmkvManager {
return if (json.isNullOrBlank()) {
mutableListOf()
} else {
Gson().fromJson(json, Array<String>::class.java).toMutableList()
JsonUtil.fromJson(json, Array<String>::class.java).toMutableList()
}
}
@@ -270,7 +270,7 @@ object MmkvManager {
assetStorage.allKeys()?.forEach { key ->
val json = assetStorage.decodeString(key)
if (!json.isNullOrBlank()) {
assetUrlItems.add(Pair(key, Gson().fromJson(json, AssetUrlItem::class.java)))
assetUrlItems.add(Pair(key, JsonUtil.fromJson(json, AssetUrlItem::class.java)))
}
}
return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
@@ -282,12 +282,12 @@ object MmkvManager {
fun encodeAsset(assetid: String, assetItem: AssetUrlItem) {
val key = assetid.ifBlank { Utils.getUuid() }
assetStorage.encode(key, Gson().toJson(assetItem))
assetStorage.encode(key, JsonUtil.toJson(assetItem))
}
fun decodeAsset(assetid: String): AssetUrlItem? {
val json = assetStorage.decodeString(assetid) ?: return null
return Gson().fromJson(json, AssetUrlItem::class.java)
return JsonUtil.fromJson(json, AssetUrlItem::class.java)
}
//endregion
@@ -297,14 +297,14 @@ object MmkvManager {
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
if (ruleset.isNullOrEmpty()) return null
return Gson().fromJson(ruleset, Array<RulesetItem>::class.java).toMutableList()
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java).toMutableList()
}
fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) {
if (rulesetList.isNullOrEmpty())
settingsStorage.encode(PREF_ROUTING_RULESET, "")
else
settingsStorage.encode(PREF_ROUTING_RULESET, Gson().toJson(rulesetList))
settingsStorage.encode(PREF_ROUTING_RULESET, JsonUtil.toJson(rulesetList))
}
//endregion

View File

@@ -3,46 +3,33 @@ package com.v2ray.ang.util
import android.content.Context
import android.os.SystemClock
import android.util.Log
import com.google.gson.Gson
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.service.ProcessService
import com.v2ray.ang.util.fmt.Hysteria2Fmt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
object PluginUtil {
//private const val HYSTERIA2 = "hysteria2-plugin"
private const val HYSTERIA2 = "libhysteria2.so"
private const val packageName = ANG_PACKAGE
private lateinit var process: Process
private const val TAG = ANG_PACKAGE
private lateinit var procService: ProcessService
// fun initPlugin(name: String): PluginManager.InitResult {
// return PluginManager.init(name)!!
// }
fun runPlugin(context: Context, config: ServerConfig?) {
Log.d(packageName, "runPlugin")
fun runPlugin(context: Context, config: ServerConfig?, domainPort: String?) {
Log.d(TAG, "runPlugin")
val outbound = config?.getProxyOutbound() ?: return
if (outbound.protocol.equals(EConfigType.HYSTERIA2.name, true)) {
Log.d(packageName, "runPlugin $HYSTERIA2")
val configFile = genConfigHy2(context, config, domainPort) ?: return
val cmd = genCmdHy2(context, configFile)
val socksPort = 100 + Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return
val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
Log.d(packageName, "runPlugin ${configFile.absolutePath}")
configFile.parentFile?.mkdirs()
configFile.writeText(Gson().toJson(hy2Config))
Log.d(packageName, Gson().toJson(hy2Config))
runHy2(context, configFile)
procService = ProcessService()
procService.runProcess(context, cmd)
}
}
@@ -50,8 +37,46 @@ object PluginUtil {
stopHy2()
}
private fun runHy2(context: Context, configFile: File) {
val cmd = mutableListOf(
fun realPingHy2(context: Context, config: ServerConfig?): Long {
Log.d(TAG, "realPingHy2")
val retFailure = -1L
val outbound = config?.getProxyOutbound() ?: return retFailure
if (outbound.protocol.equals(EConfigType.HYSTERIA2.name, true)) {
val socksPort = Utils.findFreePort(listOf(0))
val configFile = genConfigHy2(context, config, "0:${socksPort}") ?: return retFailure
val cmd = genCmdHy2(context, configFile)
val proc = ProcessService()
proc.runProcess(context, cmd)
Thread.sleep(1000L)
val delay = SpeedtestUtil.testConnection(context, socksPort)
proc.stopProcess()
return delay.first
}
return retFailure
}
private fun genConfigHy2(context: Context, config: ServerConfig, domainPort: String?): File? {
Log.d(TAG, "runPlugin $HYSTERIA2")
val socksPort = domainPort?.split(":")?.last()
.let { if (it.isNullOrEmpty()) return null else it.toInt() }
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null
val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
Log.d(TAG, "runPlugin ${configFile.absolutePath}")
configFile.parentFile?.mkdirs()
configFile.writeText(JsonUtil.toJson(hy2Config))
Log.d(TAG, JsonUtil.toJson(hy2Config))
return configFile
}
private fun genCmdHy2(context: Context, configFile: File): MutableList<String> {
return mutableListOf(
File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath,
//initPlugin(HYSTERIA2).path,
"--disable-update-check",
@@ -61,34 +86,14 @@ object PluginUtil {
"warn",
"client"
)
Log.d(packageName, cmd.toString())
try {
val proBuilder = ProcessBuilder(cmd)
proBuilder.redirectErrorStream(true)
process = proBuilder
.directory(context.filesDir)
.start()
CoroutineScope(Dispatchers.IO).launch {
Thread.sleep(500L)
Log.d(packageName, "$HYSTERIA2 check")
process.waitFor()
Log.d(packageName, "$HYSTERIA2 exited")
}
Log.d(packageName, process.toString())
} catch (e: Exception) {
Log.d(packageName, e.toString())
}
}
private fun stopHy2() {
try {
Log.d(packageName, "$HYSTERIA2 destroy")
process?.destroy()
Log.d(TAG, "$HYSTERIA2 destroy")
procService?.stopProcess()
} catch (e: Exception) {
Log.d(packageName, e.toString())
Log.d(TAG, e.toString())
}
}
}

View File

@@ -23,36 +23,19 @@ object QRCodeDecoder {
* create qrcode using zxing
*/
fun createQRCode(text: String, size: Int = 800): Bitmap? {
try {
val hints = HashMap<EncodeHintType, String>()
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
val bitMatrix = QRCodeWriter().encode(
text,
BarcodeFormat.QR_CODE, size, size, hints
)
val pixels = IntArray(size * size)
for (y in 0 until size) {
for (x in 0 until size) {
if (bitMatrix.get(x, y)) {
pixels[y * size + x] = 0xff000000.toInt()
} else {
pixels[y * size + x] = 0xffffffff.toInt()
}
}
return runCatching {
val hints = mapOf(EncodeHintType.CHARACTER_SET to Charsets.UTF_8)
val bitMatrix = QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, size, size, hints)
val pixels = IntArray(size * size) { i ->
if (bitMatrix.get(i % size, i / size)) 0xff000000.toInt() else 0xffffffff.toInt()
}
val bitmap = Bitmap.createBitmap(
size, size,
Bitmap.Config.ARGB_8888
)
bitmap.setPixels(pixels, 0, size, 0, 0, size, size)
return bitmap
} catch (e: Exception) {
e.printStackTrace()
return null
}
Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
setPixels(pixels, 0, size, 0, 0, size, size)
}
}.getOrNull()
}
/**
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
*
@@ -70,40 +53,24 @@ object QRCodeDecoder {
* @return 返回二维码图片里的内容 或 null
*/
fun syncDecodeQRCode(bitmap: Bitmap?): String? {
if (bitmap == null) {
return null
}
var source: RGBLuminanceSource? = null
try {
val width = bitmap.width
val height = bitmap.height
val pixels = IntArray(width * height)
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
source = RGBLuminanceSource(width, height, pixels)
val qrReader = QRCodeReader()
try {
val result = try {
qrReader.decode(
BinaryBitmap(GlobalHistogramBinarizer(source)),
mapOf(DecodeHintType.TRY_HARDER to true)
)
} catch (e: NotFoundException) {
qrReader.decode(
BinaryBitmap(GlobalHistogramBinarizer(source.invert())),
mapOf(DecodeHintType.TRY_HARDER to true)
)
return bitmap?.let {
runCatching {
val pixels = IntArray(it.width * it.height).also { array ->
it.getPixels(array, 0, it.width, 0, 0, it.width, it.height)
}
return result.text
} catch (e: Exception) {
e.printStackTrace()
}
} catch (e: Exception) {
e.printStackTrace()
}
val source = RGBLuminanceSource(it.width, it.height, pixels)
val qrReader = QRCodeReader()
return null
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()
}
}
/**
* 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR
*
@@ -149,6 +116,6 @@ object QRCodeDecoder {
)
HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE
HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats
HINTS[DecodeHintType.CHARACTER_SET] = "utf-8"
HINTS[DecodeHintType.CHARACTER_SET] = Charsets.UTF_8
}
}

View File

@@ -2,12 +2,19 @@ package com.v2ray.ang.util
import android.content.Context
import android.text.TextUtils
import com.google.gson.Gson
import com.v2ray.ang.AppConfig
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.RoutingType
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.util.MmkvManager.decodeProfileConfig
import com.v2ray.ang.util.MmkvManager.decodeServerConfig
import com.v2ray.ang.util.MmkvManager.decodeServerList
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils.parseInt
import java.util.Collections
object SettingsManager {
@@ -21,21 +28,41 @@ object SettingsManager {
}
private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList<RulesetItem>? {
val fileName = when (index) {
0 -> "custom_routing_white"
1 -> "custom_routing_black"
2 -> "custom_routing_global"
else -> "custom_routing_white"
}
val fileName = RoutingType.fromIndex(index).fileName
val assets = Utils.readTextFromAssets(context, fileName)
if (TextUtils.isEmpty(assets)) {
return null
}
return Gson().fromJson(assets, Array<RulesetItem>::class.java).toMutableList()
return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java).toMutableList()
}
fun resetRoutingRulesets(context: Context, index: Int) {
val rulesetList = getPresetRoutingRulesets(context, index) ?: return
resetRoutingRulesetsCommon(rulesetList)
}
fun resetRoutingRulesetsFromClipboard(content: String?): Boolean {
if (content.isNullOrEmpty()) {
return false
}
try {
val rulesetList = JsonUtil.fromJson(content, Array<RulesetItem>::class.java).toMutableList()
if (rulesetList.isNullOrEmpty()) {
return false
}
resetRoutingRulesetsCommon(rulesetList)
return true
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
private fun resetRoutingRulesetsCommon(rulesetList: MutableList<RulesetItem>) {
val rulesetNew: MutableList<RulesetItem> = mutableListOf()
MmkvManager.decodeRoutingRulesets()?.forEach { key ->
if (key.looked == true) {
@@ -43,7 +70,6 @@ object SettingsManager {
}
}
val rulesetList = getPresetRoutingRulesets(context, index) ?: return
rulesetNew.addAll(rulesetList)
MmkvManager.encodeRoutingRulesets(rulesetNew)
}
@@ -83,7 +109,9 @@ object SettingsManager {
fun routingRulesetsBypassLan(): Boolean {
val rulesetItems = MmkvManager.decodeRoutingRulesets()
val exist = rulesetItems?.any { it.enabled && it.domain?.contains(":private") == true }
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
}
@@ -117,4 +145,12 @@ object SettingsManager {
return null
}
fun getSocksPort(): Int {
return parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
}
fun getHttpPort(): Int {
return parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
}
}

View File

@@ -98,9 +98,9 @@ object SpeedtestUtil {
}
}
fun testConnection(context: Context, port: Int): String {
// return V2RayVpnService.measureV2rayDelay()
fun testConnection(context: Context, port: Int): Pair<Long, String> {
var result: String
var elapsed = -1L
var conn: HttpURLConnection? = null
try {
@@ -120,7 +120,7 @@ object SpeedtestUtil {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
val elapsed = SystemClock.elapsedRealtime() - start
elapsed = SystemClock.elapsedRealtime() - start
if (code == 204 || code == 200 && conn.responseLength == 0L) {
result = context.getString(R.string.connection_test_available, elapsed)
@@ -134,10 +134,7 @@ object SpeedtestUtil {
}
} catch (e: IOException) {
// network exception
Log.d(
AppConfig.ANG_PACKAGE,
"testConnection IOException: " + Log.getStackTraceString(e)
)
Log.d(AppConfig.ANG_PACKAGE, "testConnection IOException: " + Log.getStackTraceString(e))
result = context.getString(R.string.connection_test_error, e.message)
} catch (e: Exception) {
// library exception, eg sumsung
@@ -147,7 +144,7 @@ object SpeedtestUtil {
conn?.disconnect()
}
return result
return Pair(elapsed, result)
}
fun getLibVersion(): String {

View File

@@ -22,6 +22,7 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.dto.Language
import com.v2ray.ang.extension.toast
import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
@@ -405,21 +406,24 @@ object Utils {
}
fun getLocale(): Locale {
val lang = settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: "auto"
return when (lang) {
"auto" -> getSysLocale()
"en" -> Locale.ENGLISH
"zh-rCN" -> Locale.CHINA
"zh-rTW" -> Locale.TRADITIONAL_CHINESE
"vi" -> Locale("vi")
"ru" -> Locale("ru")
"fa" -> Locale("fa")
"bn" -> Locale("bn")
else -> getSysLocale()
val langCode = settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
val language = Language.fromCode(langCode)
return when (language) {
Language.AUTO -> getSysLocale()
Language.ENGLISH -> Locale.ENGLISH
Language.CHINA -> Locale.CHINA
Language.TRADITIONAL_CHINESE -> Locale.TRADITIONAL_CHINESE
Language.VIETNAMESE -> Locale("vi")
Language.RUSSIAN -> Locale("ru")
Language.PERSIAN -> Locale("fa")
Language.BANGLA -> Locale("bn")
}
}
private fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
LocaleList.getDefault()[0]
} else {
@@ -453,6 +457,29 @@ object Utils {
}
}
fun findFreePort(ports: List<Int>): Int {
for (port in ports) {
try {
return ServerSocket(port).use { it.localPort }
} catch (ex: IOException) {
continue // try next port
}
}
// if the program gets here, no port in the range was found
throw IOException("no free port found")
}
fun isValidSubUrl(value: String?): Boolean {
try {
if (value.isNullOrEmpty()) return false
if (URLUtil.isHttpsUrl(value)) return true
if (URLUtil.isHttpUrl(value) && value.contains(LOOPBACK)) return true
} catch (e: Exception) {
e.printStackTrace()
}
return false
}
}

View File

@@ -3,10 +3,23 @@ package com.v2ray.ang.util
import android.content.Context
import android.text.TextUtils
import android.util.Log
import com.google.gson.Gson
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.DNS_ALIDNS_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_ALIDNS_DOMAIN
import com.v2ray.ang.AppConfig.DNS_GOOGLE_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_GOOGLE_DOMAIN
import com.v2ray.ang.AppConfig.DNS_ONE_ONE_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_ONE_ONE_DOMAIN
import com.v2ray.ang.AppConfig.DNS_PUB_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_PUB_DOMAIN
import com.v2ray.ang.AppConfig.GEOIP_CN
import com.v2ray.ang.AppConfig.GEOSITE_CN
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
import com.v2ray.ang.AppConfig.GOOGLEAPIS_CN_DOMAIN
import com.v2ray.ang.AppConfig.GOOGLEAPIS_COM_DOMAIN
import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM
import com.v2ray.ang.AppConfig.TAG_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT
@@ -14,6 +27,7 @@ import com.v2ray.ang.AppConfig.TAG_FRAGMENT
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
import com.v2ray.ang.dto.ConfigResult
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServerConfig
@@ -25,34 +39,32 @@ import com.v2ray.ang.util.MmkvManager.settingsStorage
object V2rayConfigUtil {
data class Result(var status: Boolean, var content: String = "", var domainPort: String? = null)
fun getV2rayConfig(context: Context, guid: String): Result {
fun getV2rayConfig(context: Context, guid: String): ConfigResult {
try {
val config = MmkvManager.decodeServerConfig(guid) ?: return Result(false)
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
if (config.configType == EConfigType.CUSTOM) {
val raw = MmkvManager.decodeServerRaw(guid)
val customConfig = if (raw.isNullOrBlank()) {
config.fullConfig?.toPrettyPrinting() ?: return Result(false)
config.fullConfig?.toPrettyPrinting() ?: return ConfigResult(false)
} else {
raw
}
val domainPort = config.getProxyOutbound()?.getServerAddressAndPort()
return Result(true, customConfig, domainPort)
return ConfigResult(true, guid, customConfig, domainPort)
}
val result = getV2rayNonCustomConfig(context, config)
//Log.d(ANG_PACKAGE, result.content)
Log.d(ANG_PACKAGE, result.domainPort?:"")
result.guid = guid
return result
} catch (e: Exception) {
e.printStackTrace()
return Result(false)
return ConfigResult(false)
}
}
private fun getV2rayNonCustomConfig(context: Context, config: ServerConfig): Result {
val result = Result(false)
private fun getV2rayNonCustomConfig(context: Context, config: ServerConfig): ConfigResult {
val result = ConfigResult(false)
val outbound = config.getProxyOutbound() ?: return result
val address = outbound.getServerAddress() ?: return result
@@ -68,8 +80,9 @@ object V2rayConfigUtil {
if (TextUtils.isEmpty(assets)) {
return result
}
val v2rayConfig = Gson().fromJson(assets, V2rayConfig::class.java) ?: return result
v2rayConfig.log.loglevel = settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL) ?: "warning"
val v2rayConfig = JsonUtil.fromJson(assets, V2rayConfig::class.java) ?: return result
v2rayConfig.log.loglevel =
settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL) ?: "warning"
v2rayConfig.remarks = config.remarks
inbounds(v2rayConfig)
@@ -101,14 +114,8 @@ object V2rayConfigUtil {
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()
)
val socksPort = SettingsManager.getSocksPort()
val httpPort = SettingsManager.getHttpPort()
v2rayConfig.inbounds.forEach { curInbound ->
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) {
@@ -147,9 +154,13 @@ object V2rayConfigUtil {
return true
}
private fun outbounds(v2rayConfig: V2rayConfig, outbound: V2rayConfig.OutboundBean, isPlugin: Boolean): Pair<Boolean, String> {
private fun outbounds(
v2rayConfig: V2rayConfig,
outbound: V2rayConfig.OutboundBean,
isPlugin: Boolean
): Pair<Boolean, String> {
if (isPlugin) {
val socksPort = 100 + Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
val outboundNew = V2rayConfig.OutboundBean(
mux = null,
protocol = EConfigType.SOCKS.name.lowercase(),
@@ -194,7 +205,9 @@ object V2rayConfigUtil {
private fun routing(v2rayConfig: V2rayConfig): Boolean {
try {
v2rayConfig.routing.domainStrategy = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "IPIfNonMatch"
v2rayConfig.routing.domainStrategy =
settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
?: "IPIfNonMatch"
val rulesetItems = MmkvManager.decodeRoutingRulesets()
rulesetItems?.forEach { key ->
@@ -213,7 +226,7 @@ object V2rayConfigUtil {
return
}
val rule = Gson().fromJson(Gson().toJson(item), RulesBean::class.java) ?: return
val rule = JsonUtil.fromJson(JsonUtil.toJson(item), RulesBean::class.java) ?: return
v2rayConfig.routing.rules.add(rule)
@@ -229,7 +242,9 @@ object V2rayConfigUtil {
rulesetItems?.forEach { key ->
if (key != null && key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) {
key.domain?.forEach {
if (it.startsWith("geosite:") || it.startsWith("domain:")) {
if (it != GEOSITE_PRIVATE
&& (it.startsWith("geosite:") || it.startsWith("domain:"))
) {
domain.add(it)
}
}
@@ -242,7 +257,7 @@ object V2rayConfigUtil {
private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean {
try {
if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
val geositeCn = arrayListOf("geosite:cn")
val geositeCn = arrayListOf(GEOSITE_CN)
val proxyDomain = userRule2Domain(TAG_PROXY)
val directDomain = userRule2Domain(TAG_DIRECT)
// fakedns with all domains to make it always top priority
@@ -295,7 +310,7 @@ object V2rayConfigUtil {
// DNS routing tag
v2rayConfig.routing.rules.add(
0, V2rayConfig.RoutingBean.RulesBean(
0, RulesBean(
inboundTag = arrayListOf("dns-in"),
outboundTag = "dns-out",
domain = null
@@ -333,8 +348,8 @@ object V2rayConfigUtil {
// domestic DNS
val domesticDns = Utils.getDomesticDnsServers()
val directDomain = userRule2Domain(TAG_DIRECT)
val isCnRoutingMode = directDomain.contains("geosite:cn")
val geoipCn = arrayListOf("geoip:cn")
val isCnRoutingMode = directDomain.contains(GEOSITE_CN)
val geoipCn = arrayListOf(GEOIP_CN)
if (directDomain.size > 0) {
servers.add(
V2rayConfig.DnsBean.ServersBean(
@@ -348,7 +363,7 @@ object V2rayConfigUtil {
if (Utils.isPureIpAddress(domesticDns.first())) {
v2rayConfig.routing.rules.add(
0, V2rayConfig.RoutingBean.RulesBean(
0, RulesBean(
outboundTag = TAG_DIRECT,
port = "53",
ip = arrayListOf(domesticDns.first()),
@@ -364,13 +379,14 @@ object V2rayConfigUtil {
}
// hardcode googleapi rule to fix play store problems
hosts["domain:googleapis.cn"] = "googleapis.com"
hosts[GOOGLEAPIS_CN_DOMAIN] = GOOGLEAPIS_COM_DOMAIN
// hardcode popular Android Private DNS rule to fix localhost DNS problem
hosts["dns.pub"] = arrayListOf("1.12.12.12", "120.53.53.53")
hosts["dns.alidns.com"] = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
hosts["one.one.one.one"] = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
hosts["dns.google"] = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
hosts[DNS_PUB_DOMAIN] = DNS_PUB_ADDRESSES
hosts[DNS_ALIDNS_DOMAIN] = DNS_ALIDNS_ADDRESSES
hosts[DNS_ONE_ONE_DOMAIN] = DNS_ONE_ONE_ADDRESSES
hosts[DNS_GOOGLE_DOMAIN] = DNS_GOOGLE_ADDRESSES
// DNS dns对象
v2rayConfig.dns = V2rayConfig.DnsBean(
@@ -381,7 +397,7 @@ object V2rayConfigUtil {
// DNS routing
if (Utils.isPureIpAddress(remoteDns.first())) {
v2rayConfig.routing.rules.add(
0, V2rayConfig.RoutingBean.RulesBean(
0, RulesBean(
outboundTag = TAG_PROXY,
port = "53",
ip = arrayListOf(remoteDns.first()),
@@ -447,7 +463,7 @@ object V2rayConfigUtil {
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(
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
requestString,
V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
)
@@ -534,7 +550,11 @@ object V2rayConfigUtil {
return true
}
private fun moreOutbounds(v2rayConfig: V2rayConfig, subscriptionId: String, isPlugin: Boolean): Pair<Boolean, String> {
private fun moreOutbounds(
v2rayConfig: V2rayConfig,
subscriptionId: String,
isPlugin: Boolean
): Pair<Boolean, String> {
val returnPair = Pair(false, "")
var domainPort: String = ""
@@ -546,7 +566,7 @@ object V2rayConfigUtil {
return returnPair
}
if (subscriptionId.isNullOrEmpty()) {
if (subscriptionId.isEmpty()) {
return returnPair
}
try {

View File

@@ -0,0 +1,104 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.util.Utils
open class FmtBase {
fun toUri(address: String?, port: Int?, userInfo: String?, dicQuery: HashMap<String, String>?, remark: String): 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(address),
port
)
return "${url}${query}#${Utils.urlEncode(remark)}"
}
fun getStdTransport(outbound: V2rayConfig.OutboundBean, streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean): HashMap<String, String> {
val dicQuery = HashMap<String, String>()
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
(streamSetting.tlsSettings
?: streamSetting.realitySettings)?.let { tlsSetting ->
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
dicQuery["sni"] = tlsSetting.serverName
}
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
dicQuery["alpn"] =
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
dicQuery["fp"] = tlsSetting.fingerprint.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
dicQuery["pbk"] = tlsSetting.publicKey.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
dicQuery["sid"] = tlsSetting.shortId.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
dicQuery["spx"] = tlsSetting.spiderX.orEmpty()
}
}
dicQuery["type"] =
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
outbound.getTransportSettingDetails()?.let { transportDetails ->
when (streamSetting.network) {
"tcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = transportDetails[1]
}
}
"kcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["seed"] = transportDetails[2]
}
}
"ws", "httpupgrade", "splithttp" -> {
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = transportDetails[1]
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = transportDetails[2]
}
}
"http", "h2" -> {
dicQuery["type"] = "http"
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = transportDetails[1]
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = transportDetails[2]
}
}
"quic" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
dicQuery["quicSecurity"] = transportDetails[1]
dicQuery["key"] = transportDetails[2]
}
"grpc" -> {
dicQuery["mode"] = transportDetails[0]
dicQuery["authority"] = transportDetails[1]
dicQuery["serviceName"] = transportDetails[2]
}
}
}
return dicQuery
}
}

View File

@@ -12,10 +12,10 @@ import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import java.net.URI
object Hysteria2Fmt {
object Hysteria2Fmt : FmtBase() {
fun parse(str: String): ServerConfig {
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val allowInsecure = settingsStorage.decodeBool(AppConfig.PREF_ALLOW_INSECURE,false)
val config = ServerConfig.create(EConfigType.HYSTERIA2)
val uri = URI(Utils.fixIllegalUrl(str))
@@ -26,7 +26,7 @@ object Hysteria2Fmt {
config.outboundBean?.streamSettings?.populateTlsSettings(
V2rayConfig.TLS,
if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure,
if ((queryParam["insecure"].orEmpty()) == "1") true else allowInsecure,
queryParam["sni"] ?: uri.idnHost,
null,
queryParam["alpn"],
@@ -40,6 +40,10 @@ object Hysteria2Fmt {
server.port = uri.port
server.password = uri.userInfo
}
if (!queryParam["obfs-password"].isNullOrEmpty()) {
config.outboundBean?.settings?.obfsPassword = queryParam["obfs-password"]
}
return config
}
@@ -47,7 +51,7 @@ object Hysteria2Fmt {
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
streamSetting.tlsSettings?.let { tlsSetting ->
@@ -59,29 +63,35 @@ object Hysteria2Fmt {
dicQuery["alpn"] = Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
}
}
if (!outbound.settings?.obfsPassword.isNullOrEmpty()) {
dicQuery["obfs"] = "salamander"
dicQuery["obfs-password"] = outbound.settings?.obfsPassword ?: ""
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
outbound.getPassword(),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + query + remark
return toUri(outbound.getServerAddress(), outbound.getServerPort(), outbound.getPassword(), dicQuery, config.remarks)
}
fun toNativeConfig(config: ServerConfig, socksPort: Int): Hysteria2Bean? {
val outbound = config.getProxyOutbound() ?: return null
val tls = outbound.streamSettings?.tlsSettings
val obfs = if (outbound.settings?.obfsPassword.isNullOrEmpty()) null else
Hysteria2Bean.ObfsBean(
type = "salamander",
salamander = Hysteria2Bean.ObfsBean.SalamanderBean(
password = outbound.settings?.obfsPassword
)
)
val bean = Hysteria2Bean(
server = outbound.getServerAddressAndPort(),
auth = outbound.getPassword(),
obfs = obfs,
socks5 = Hysteria2Bean.Socks5Bean(
listen = "$LOOPBACK:${socksPort}",
),
http = Hysteria2Bean.Socks5Bean(
listen = "$LOOPBACK:${socksPort}",
),
tls = Hysteria2Bean.TlsBean(
sni = tls?.serverName ?: outbound.getServerAddress(),
insecure = tls?.allowInsecure

View File

@@ -8,7 +8,7 @@ import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.Utils
import java.net.URI
object ShadowsocksFmt {
object ShadowsocksFmt : FmtBase() {
fun parse(str: String): ServerConfig? {
val config = ServerConfig.create(EConfigType.SHADOWSOCKS)
if (!tryResolveResolveSip002(str, config)) {
@@ -52,16 +52,10 @@ object ShadowsocksFmt {
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val remark = "#" + Utils.urlEncode(config.remarks)
val pw =
Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
val url = String.format(
"%s@%s:%s",
pw,
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + remark
val pw = Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
return toUri(outbound.getServerAddress(), outbound.getServerPort(), pw, null, config.remarks)
}
private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {

View File

@@ -5,7 +5,7 @@ import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.util.Utils
object SocksFmt {
object SocksFmt : FmtBase() {
fun parse(str: String): ServerConfig? {
val config = ServerConfig.create(EConfigType.SOCKS)
var result = str.replace(EConfigType.SOCKS.protocolScheme, "")
@@ -52,18 +52,13 @@ object SocksFmt {
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val remark = "#" + Utils.urlEncode(config.remarks)
val pw =
if (outbound.settings?.servers?.get(0)?.users?.get(0)?.user != null)
"${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}"
else
":"
val url = String.format(
"%s@%s:%s",
Utils.encode(pw),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + remark
return toUri(outbound.getServerAddress(), outbound.getServerPort(), pw, null, config.remarks)
}
}

View File

@@ -10,7 +10,7 @@ import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import java.net.URI
object TrojanFmt {
object TrojanFmt : FmtBase() {
fun parse(str: String): ServerConfig {
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
@@ -76,98 +76,15 @@ object TrojanFmt {
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
val dicQuery = getStdTransport(outbound, streamSetting)
config.outboundBean?.settings?.servers?.get(0)?.flow?.let {
if (!TextUtils.isEmpty(it)) {
dicQuery["flow"] = it
}
}
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
(streamSetting.tlsSettings
?: streamSetting.realitySettings)?.let { tlsSetting ->
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
dicQuery["sni"] = tlsSetting.serverName
}
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
dicQuery["alpn"] =
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
dicQuery["fp"] = tlsSetting.fingerprint.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
dicQuery["pbk"] = tlsSetting.publicKey.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
dicQuery["sid"] = tlsSetting.shortId.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX.orEmpty())
}
}
dicQuery["type"] =
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
outbound.getTransportSettingDetails()?.let { transportDetails ->
when (streamSetting.network) {
"tcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
}
"kcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
}
}
"ws", "httpupgrade", "splithttp" -> {
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"http", "h2" -> {
dicQuery["type"] = "http"
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"quic" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
}
"grpc" -> {
dicQuery["mode"] = transportDetails[0]
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
}
}
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
outbound.getPassword(),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + query + remark
return toUri(outbound.getServerAddress(), outbound.getServerPort(), outbound.getPassword(), dicQuery, config.remarks)
}
}

View File

@@ -10,7 +10,7 @@ import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import java.net.URI
object VlessFmt {
object VlessFmt : FmtBase() {
fun parse(str: String): ServerConfig? {
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
@@ -63,8 +63,9 @@ object VlessFmt {
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
val dicQuery = getStdTransport(outbound, streamSetting)
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let {
if (!TextUtils.isEmpty(it)) {
dicQuery["flow"] = it
@@ -74,91 +75,6 @@ object VlessFmt {
if (outbound.getSecurityEncryption().isNullOrEmpty()) "none"
else outbound.getSecurityEncryption().orEmpty()
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
(streamSetting.tlsSettings
?: streamSetting.realitySettings)?.let { tlsSetting ->
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
dicQuery["sni"] = tlsSetting.serverName
}
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
dicQuery["alpn"] =
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
dicQuery["fp"] = tlsSetting.fingerprint.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
dicQuery["pbk"] = tlsSetting.publicKey.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
dicQuery["sid"] = tlsSetting.shortId.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX.orEmpty())
}
}
dicQuery["type"] =
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
outbound.getTransportSettingDetails()?.let { transportDetails ->
when (streamSetting.network) {
"tcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
}
"kcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
}
}
"ws", "httpupgrade", "splithttp" -> {
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"http", "h2" -> {
dicQuery["type"] = "http"
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"quic" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
}
"grpc" -> {
dicQuery["mode"] = transportDetails[0]
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
}
}
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
outbound.getPassword(),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + query + remark
return toUri(outbound.getServerAddress(), outbound.getServerPort(), outbound.getPassword(), dicQuery, config.remarks)
}
}

View File

@@ -2,18 +2,19 @@ package com.v2ray.ang.util.fmt
import android.text.TextUtils
import android.util.Log
import com.google.gson.Gson
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.VmessQRCode
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import java.net.URI
object VmessFmt {
object VmessFmt : FmtBase() {
fun parse(str: String): ServerConfig? {
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
@@ -29,7 +30,7 @@ object VmessFmt {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
return null
}
val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java)
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)
@@ -100,7 +101,7 @@ object VmessFmt {
vmessQRCode.host = transportDetails[1]
vmessQRCode.path = transportDetails[2]
}
val json = Gson().toJson(vmessQRCode)
val json = JsonUtil.toJson(vmessQRCode)
return Utils.encode(json)
}

View File

@@ -8,7 +8,7 @@ import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.util.Utils
import java.net.URI
object WireguardFmt {
object WireguardFmt : FmtBase() {
fun parse(str: String): ServerConfig? {
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery != null) {
@@ -71,37 +71,22 @@ object WireguardFmt {
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
dicQuery["publickey"] =
Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString())
dicQuery["publickey"] = outbound.settings?.peers?.get(0)?.publicKey.toString()
if (outbound.settings?.reserved != null) {
dicQuery["reserved"] = Utils.urlEncode(
Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString(","))
.toString()
)
dicQuery["reserved"] = Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString(",")).toString()
}
dicQuery["address"] = Utils.urlEncode(
Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString(","))
.toString()
)
dicQuery["address"] = Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString(",")).toString()
if (outbound.settings?.mtu != null) {
dicQuery["mtu"] = outbound.settings?.mtu.toString()
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
Utils.urlEncode(outbound.getPassword().toString()),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + query + remark
return toUri(outbound.getServerAddress(), outbound.getServerPort(), outbound.getPassword(), dicQuery, config.remarks)
}
}

View File

@@ -11,7 +11,6 @@ import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.google.gson.Gson
import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
@@ -21,14 +20,14 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.serializable
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.V2rayConfigUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -43,7 +42,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
var subscriptionId: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
//var keywordFilter: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
var keywordFilter = ""
var keywordFilter = ""
val serversCache = mutableListOf<ServersCache>()
val isRunning by lazy { MutableLiveData<Boolean>() }
val updateListAction by lazy { MutableLiveData<Int>() }
@@ -98,7 +97,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
try {
val config = ServerConfig.create(EConfigType.CUSTOM)
config.subscriptionId = subscriptionId
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
config.fullConfig = JsonUtil.fromJson(server, V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
@@ -153,17 +152,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
fun updateConfigViaSubAll(): Int {
if (subscriptionId.isNullOrEmpty()) {
if (subscriptionId.isEmpty()) {
return AngConfigManager.updateConfigViaSubAll()
} else {
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return 0
return updateConfigViaSub(Pair(subscriptionId, subItem))
return AngConfigManager.updateConfigViaSub(Pair(subscriptionId, subItem))
}
}
fun exportAllServer(): Int {
val serverListCopy =
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
serverList
} else {
serversCache.map { it.guid }.toList()
@@ -181,10 +180,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
SpeedtestUtil.closeAllTcpSockets()
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
updateListAction.value = -1 // update all
//updateListAction.value = -1 // update all
getApplication<AngApplication>().toast(R.string.connection_test_testing)
for (item in serversCache) {
val serversCopy = serversCache.toList() // Create a copy of the list
for (item in serversCopy) {
item.profile.let { outbound ->
val serverAddress = outbound.server
val serverPort = outbound.serverPort
@@ -207,18 +206,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
updateListAction.value = -1 // update all
val serversCopy = serversCache.toList() // Create a copy of the list
getApplication<AngApplication>().toast(R.string.connection_test_testing)
viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread
for (item in serversCopy) {
val config = V2rayConfigUtil.getV2rayConfig(getApplication(), item.guid)
if (config.status) {
MessageUtil.sendMsg2TestService(
getApplication(),
AppConfig.MSG_MEASURE_CONFIG,
Pair(item.guid, config.content)
)
}
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, item.guid)
}
}
}
@@ -288,7 +278,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
fun removeAllServer() {
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
MmkvManager.removeAllServer()
} else {
val serversCopy = serversCache.toList()
@@ -299,7 +289,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
fun removeInvalidServer() {
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
MmkvManager.removeInvalidServer("")
} else {
val serversCopy = serversCache.toList()
@@ -394,15 +384,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
val resultPair: Pair<String, Long> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getSerializableExtra("content", Pair::class.java) as Pair<String, Long>
} else {
intent.getSerializableExtra("content") as Pair<String, Long>
}
val resultPair = intent.serializable<Pair<String, Long>>("content") ?: return
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
updateListAction.value = getPosition(resultPair.first)
}
}
}
}
}
}

View File

@@ -33,17 +33,13 @@
android:gravity="center"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_pref_per_app_proxy"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_per_app_proxy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_start" />
android:maxLines="2"
android:text="@string/title_pref_per_app_proxy"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</LinearLayout>
@@ -56,18 +52,12 @@
android:gravity="center"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_bypass_apps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/switch_bypass_apps_mode"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_bypass_apps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_start" />
android:text="@string/switch_bypass_apps_mode"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</LinearLayout>
</LinearLayout>

View File

@@ -89,7 +89,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="24dp">
android:layout_marginBottom="12dp">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"

View File

@@ -13,6 +13,25 @@
<include layout="@layout/layout_address_port" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_margin_top_height"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_obfs_password" />
<EditText
android:id="@+id/et_obfs_password"
android:layout_width="match_parent"
android:layout_height="@dimen/edit_height"
android:inputType="text" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -32,7 +51,6 @@
</LinearLayout>
<include layout="@layout/layout_tls_hysteria2" />
<LinearLayout

View File

@@ -14,5 +14,13 @@
android:id="@+id/import_rulesets"
android:title="@string/routing_settings_import_rulesets"
app:showAsAction="never" />
<item
android:id="@+id/import_rulesets_from_clipboard"
android:title="@string/routing_settings_import_rulesets_from_clipboard"
app:showAsAction="never" />
<item
android:id="@+id/export_rulesets_to_clipboard"
android:title="@string/routing_settings_export_rulesets_to_clipboard"
app:showAsAction="never" />
</menu>

View File

@@ -99,6 +99,7 @@
<string name="server_lab_content">المحتوى</string>
<string name="toast_none_data_clipboard">لا توجد بيانات في الحافظة</string>
<string name="toast_invalid_url">رابط URL غير صالح</string>
<string name="toast_insecure_url_protocol">Please do not use the insecure HTTP protocol subscription address</string>
<string name="server_lab_need_inbound">تأكد من أن منفذ الاتصالات الواردة يتوافق مع الإعدادات</string>
<string name="toast_malformed_josn">تكوين مشوه</string>
<string name="server_lab_request_host6">مضيف (SNI) (اختياري)</string>
@@ -112,6 +113,7 @@
<string name="msg_file_not_found">الملف غير موجود</string>
<string name="msg_remark_is_duplicate">الملاحظات موجودة بالفعل</string>
<string name="toast_action_not_allowed">الإجراء غير مسموح به</string>
<string name="server_obfs_password">Obfs password</string>
<!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">جار التحميل</string>
@@ -260,6 +262,8 @@
<string name="routing_settings_add_rule">Add rule</string>
<string name="routing_settings_import_rulesets">Import ruleset</string>
<string name="routing_settings_import_rulesets_tip">Existing rulesets will be deleted, are you sure to continue?</string>
<string name="routing_settings_import_rulesets_from_clipboard">Import ruleset from clipboard</string>
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="connection_test_pending">التحقق من الاتصال</string>

View File

@@ -98,6 +98,7 @@
<string name="server_lab_content">কনটেন্ট</string>
<string name="toast_none_data_clipboard">ক্লিপবোর্ডে কোনও তথ্য নেই</string>
<string name="toast_invalid_url">অবৈধ URL</string>
<string name="toast_insecure_url_protocol">Please do not use the insecure HTTP protocol subscription address</string>
<string name="server_lab_need_inbound">ইনবাউন্ড পোর্ট নিশ্চিত করুন সেটিংসের সাথে সামঞ্জস্যপূর্ণ</string>
<string name="toast_malformed_josn">কনফিগারেশন বিকৃত</string>
<string name="server_lab_request_host6">হোস্ট (SNI) (ঐচ্ছিক)</string>
@@ -111,6 +112,8 @@
<string name="msg_file_not_found">ফাইল খুঁজে পাওয়া যায়নি</string>
<string name="msg_remark_is_duplicate">মন্তব্য ইতিমধ্যে বিদ্যমান</string>
<string name="toast_action_not_allowed">অ্যাকশন অনুমোদিত নয়</string>
<string name="server_obfs_password">Obfs password</string>
<!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">লোড হচ্ছে</string>
<string name="menu_item_search">অনুসন্ধান করুন</string>
@@ -257,6 +260,8 @@
<string name="routing_settings_add_rule">Add rule</string>
<string name="routing_settings_import_rulesets">Import ruleset</string>
<string name="routing_settings_import_rulesets_tip">Existing rulesets will be deleted, are you sure to continue?</string>
<string name="routing_settings_import_rulesets_from_clipboard">Import ruleset from clipboard</string>
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="connection_test_pending">সংযোগ পরীক্ষা করুন</string>
@@ -296,4 +301,13 @@
<item>লাইট</item>
<item>ডার্ক</item>
</string-array>
<string-array name="preset_rulesets">
<item>চায়না হোয়াইটলিস্ট</item>
<item>চায়না ব্ল্যাকলিস্ট</item>
<item>গ্লোবাল</item>
<item>ইরান হোয়াইটলিস্ট</item>
</string-array>
</resources>

View File

@@ -97,6 +97,7 @@
<string name="server_lab_content">محتوا</string>
<string name="toast_none_data_clipboard">هیچ داده‌ای در کلیپ‌بورد وجود ندارد</string>
<string name="toast_invalid_url">نشانی اینترنتی معتبر نیست</string>
<string name="toast_insecure_url_protocol">Please do not use the insecure HTTP protocol subscription address</string>
<string name="server_lab_need_inbound">اطمینان حاصل کنید که پورت ورودی با تنظیمات مطابقت دارد</string>
<string name="toast_malformed_josn">کانفیگ درست نیست</string>
<string name="server_lab_request_host6">میزبان (SNI) (اختیاری)</string>
@@ -105,6 +106,7 @@
<string name="title_url">URL</string>
<string name="menu_item_download_file">دانلود فایل‌ها</string>
<string name="toast_action_not_allowed">این عمل ممنوع است</string>
<string name="server_obfs_password">Obfs password</string>
<!-- PerAppProxyActivity -->
<string name="title_user_asset_add_url">URL را اضافه کنید</string>
@@ -125,13 +127,13 @@
<string name="title_advanced">تنظیمات پیشرفته</string>
<string name="title_vpn_settings">تنظیمات VPN</string>
<string name="title_pref_per_app_proxy">پروکسی به تفکیک برنامه</string>
<string name="summary_pref_per_app_proxy">عمومی: برنامه بررسی شده پروکسی است، اتصال مستقیم بدون بررسی است. \nحالت bypass: برنامه بررسی شده مستقیما متصل است، پراکسی بررسی نشده است. \nگزینهای برای انتخاب خودکار پروکسی برنامه در منو است</string>
<string name="title_pref_is_booted">Auto connect at startup</string>
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
<string name="summary_pref_per_app_proxy">عمومی: برنامه بررسی شده پروکسی است، اتصال مستقیم بدون بررسی است. \nحالت bypass: برنامه بررسی شده مستقیما متصل است، پراکسی بررسی نشده است. \nگزینهای برای انتخاب خودکار پروکسی برنامه در منو است.</string>
<string name="title_pref_is_booted">اتصال خودکار هنگام راه اندازی</string>
<string name="summary_pref_is_booted">هنگام راه اندازی به طور خودکار به سرور انتخابی متصل می شود که ممکن است ناموفق باشد.</string>
<string name="title_mux_settings">تنظیمات Mux</string>
<string name="title_pref_mux_enabled">فعال کردن Mux</string>
<string name="summary_pref_mux_enabled">سریعتر است، اما ممکن است باعث اتصال ناپایدار شود\nمخزن ترافیک TCP با 8 اتصال پیش‌فرض، نحوه مدیریت UDP و QUIC را در زیر سفارشی کنید</string>
<string name="summary_pref_mux_enabled">سریعتر است، اما ممکن است باعث اتصال ناپایدار شود\nمخزن ترافیک TCP با 8 اتصال پیش‌فرض، نحوه مدیریت UDP و QUIC را در زیر سفارشی کنید.</string>
<string name="title_pref_mux_concurency">اتصالات TCP (محدوده -1 تا 1024)</string>
<string name="title_pref_mux_xudp_concurency">اتصالات XUDP (محدوده -1 تا 1024)</string>
<string name="title_pref_mux_xudp_quic">مدیریت QUIC در تونل mux</string>
@@ -145,16 +147,16 @@
<string name="summary_pref_speed_enabled">نمایش سرعت فعلی در قسمت آگاه‌سازی. \nآیکون آگاه‌سازی بر اساس استفاده تغییر می‌کند.</string>
<string name="title_pref_sniffing_enabled">فعال کردن Sniffing</string>
<string name="summary_pref_sniffing_enabled">دامنه sniff را از بسته امتحان کنید (پیش‌فرض روشن)</string>
<string name="title_pref_route_only_enabled">Enable routeOnly</string>
<string name="summary_pref_route_only_enabled">Use the sniffed domain name for routing only, and keep the target address as the IP address.</string>
<string name="summary_pref_sniffing_enabled">دامنه sniff را از بسته امتحان کنید (پیش‌فرض روشن).</string>
<string name="title_pref_route_only_enabled">فعال کردن routeOnly</string>
<string name="summary_pref_route_only_enabled">از نام دامنه sniffed فقط برای مسیریابی استفاده کنید و آدرس مورد نظر را به عنوان آدرس IP نگه دارید.</string>
<string name="title_pref_local_dns_enabled">فعال کردن DNS محلی</string>
<string name="summary_pref_local_dns_enabled">DNS پردازش شده توسط ماژول DNS هسته (توصیه میشود، در صورت نیاز به دور زدن LAN و نشانی mainland)</string>
<string name="summary_pref_local_dns_enabled">درخواست های DNS به هسته وارد شده و توسط ماژول DNS پردازش می شوند(توصیه می شود در صورت نیاز به مسیریابی برای دور زدن آدرس های LAN و سرزمین اصلی فعال شود)</string>
<string name="title_pref_fake_dns_enabled">فعال کردن DNS جعلی</string>
<string name="summary_pref_fake_dns_enabled">DNS محلی آدرس IP جعلی را برمی‌گرداند (سریعتر میباشد، اما ممکن است برای برخی از برنامهها کار نکند)</string>
<string name="summary_pref_fake_dns_enabled">DNS محلی آدرس های آیپی فیک را برمی‌گرداند(سریعتر می باشد اما ممکن است برای برخی از برنامه ها کار نکند)</string>
<string name="title_pref_prefer_ipv6">ترجیح دادن IPv6</string>
<string name="summary_pref_prefer_ipv6">ترجیح دادن نشانی و مسیر های IPv6</string>
@@ -171,11 +173,11 @@
<string name="summary_pref_delay_test_url">Url</string>
<string name="title_pref_proxy_sharing_enabled">اجازه اتصالات از طریق LAN</string>
<string name="summary_pref_proxy_sharing_enabled">دستگاه‌های دیگر می‌توانند از طریق socks/http به پراکسی توسط نشانی آی‌پی شما متصل شوند، فقط در شبکه مورد اعتماد فعال می‌شوند تا از اتصال غیرمجاز جلوگیری کنند</string>
<string name="summary_pref_proxy_sharing_enabled">دستگاه‌های دیگر می‌توانند از طریق socks/http به پراکسی توسط نشانی آی‌پی شما متصل شوند، فقط در شبکه مورد اعتماد فعال می‌شوند تا از اتصال غیرمجاز جلوگیری کنند.</string>
<string name="toast_warning_pref_proxysharing_short">اتصالات از طریق LAN را مجاز کنید، مطمئن شوید که در یک شبکه قابل اعتماد هستید</string>
<string name="title_pref_allow_insecure">allowInsecure</string>
<string name="summary_pref_allow_insecure">هنگام استفاده از TLS، به طور پیش‌فرض allowInsecure فعال است</string>
<string name="title_pref_allow_insecure">مجوز ناامن</string>
<string name="summary_pref_allow_insecure">هنگام استفاده از TLS، به طور پیش‌فرض مجوز ناامن فعال است.</string>
<string name="title_pref_socks_port">پورت پروکسی SOCKS5</string>
<string name="summary_pref_socks_port">پورت پروکسی SOCKS5</string>
@@ -199,17 +201,17 @@
<string name="title_privacy_policy">حریم خصوصی</string>
<string name="title_about">درباره</string>
<string name="title_source_code">Source code</string>
<string name="title_tg_channel">Telegram channel</string>
<string name="title_configuration_backup">Backup configuration</string>
<string name="summary_configuration_backup">Storage location: [%s], The backup will be cleared after uninstalling the app or clearing the storage</string>
<string name="title_configuration_restore">Restore configuration</string>
<string name="title_configuration_share">Share configuration</string>
<string name="title_source_code">کد منبع</string>
<string name="title_tg_channel">کانال تلگرام</string>
<string name="title_configuration_backup">پشتیبان گیری از پیکربندی</string>
<string name="summary_configuration_backup">محل ذخیره سازی: [%s], پس از حذف نصب برنامه یا پاک کردن فضای ذخیره سازی، نسخه پشتیبان پاک می شود</string>
<string name="title_configuration_restore">بازیابی پیکربندی</string>
<string name="title_configuration_share">اشتراک گذاری پیکربندی</string>
<string name="title_pref_promotion">تبلیغات</string>
<string name="summary_pref_promotion">تبلیغات، برای جزئیات بیشتر کلیک کنید (کمک مالی کنید تا حذف شود)</string>
<string name="title_pref_auto_update_subscription">به‌روزرسانی خودکار اشتراک ها</string>
<string name="summary_pref_auto_update_subscription">اشتراک های خود را به طور خودکار با فاصله زمانی در پس زمینه به روز کنید. بسته به دستگاه، این ویژگی ممکن است همیشه کار نکند</string>
<string name="summary_pref_auto_update_subscription">اشتراک های خود را به طور خودکار با فاصله زمانی در پس زمینه به روز کنید. بسته به دستگاه، این ویژگی ممکن است همیشه کار نکند.</string>
<string name="title_pref_auto_update_interval">فاصله به‌روزرسانی خودکار (دقیقه، حداقل مقدار 15)</string>
<string name="title_core_loglevel">سطح گزارشات</string>
<string name="title_mode">حالت</string>
@@ -222,11 +224,11 @@
<string name="logcat_copy">کپی</string>
<string name="logcat_clear">پاک کردن</string>
<string name="title_service_restart">راه‌اندازی مجدد خدمات</string>
<string name="title_del_all_config">حذف تمام کانفیگ</string>
<string name="title_del_duplicate_config">حذف کانفیگ های تکراری</string>
<string name="title_del_invalid_config">حذف کانفیگهای نامعتبر (ابتدا آزمایش کنید)</string>
<string name="title_export_all">خروجی گرفتن کانفیگهای غیرسفارشی در کلیپ‌بورد</string>
<string name="title_sub_setting">تنظیمات گروه‌ی اشتراک</string>
<string name="title_del_all_config">حذف تمام کانفیگ های گروه فعلی</string>
<string name="title_del_duplicate_config">حذف کانفیگ های تکراری گروه فعلی</string>
<string name="title_del_invalid_config">حذف کانفیگ های نامعتبر گروه فعلی (ابتدا آزمایش کنید)</string>
<string name="title_export_all">خروجی گرفتن کانفیگ های غیرسفارشی گروه فعلی در کلیپ‌بورد</string>
<string name="title_sub_setting">تنظیمات گروه‌ اشتراک</string>
<string name="sub_setting_remarks">ملاحظات</string>
<string name="sub_setting_url">نشانی اینترنتی اختیاری</string>
<string name="sub_setting_filter">Remarks regular filter</string>
@@ -235,9 +237,9 @@
<string name="sub_setting_pre_profile">Previous proxy remarks</string>
<string name="sub_setting_next_profile">Next proxy remarks</string>
<string name="sub_setting_pre_profile_tip">The remarks exists and is unique</string>
<string name="title_sub_update">به‌روزرسانی اشتراک</string>
<string name="title_ping_all_server">Tcping همه کانفیگ</string>
<string name="title_real_ping_all_server">تاخیر واقعی همه کانفیگ</string>
<string name="title_sub_update">به‌روزرسانی گروه فعلی اشتراک</string>
<string name="title_ping_all_server">Tcping کانفیگ های گروه فعلی</string>
<string name="title_real_ping_all_server">تاخیر واقعی کانفیگ های گروه فعلی</string>
<string name="title_user_asset_setting">فایل‌های دارایی جغرافیا</string>
<string name="title_sort_by_test_results">مرتب‌سازی بر اساس نتایج آزمایش</string>
<string name="title_filter_config">فیلتر کردن کانفیگ‌ها</string>
@@ -251,12 +253,14 @@
<string name="routing_settings_title">تنظیمات مسیریابی</string>
<string name="routing_settings_tips">با کاما (,) از هم جدا شوند، ذخیره کردن فراموش نشود</string>
<string name="routing_settings_save">ذخیره</string>
<string name="routing_settings_delete">پاک کردن</string>
<string name="routing_settings_rule_title">Routing Rule Settings</string>
<string name="routing_settings_add_rule">Add rule</string>
<string name="routing_settings_import_rulesets">Import ruleset</string>
<string name="routing_settings_import_rulesets_tip">Existing rulesets will be deleted, are you sure to continue?</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="routing_settings_delete">حذف</string>
<string name="routing_settings_rule_title">تنظیمات قانون مسیریابی</string>
<string name="routing_settings_add_rule">اضافه کردن قانون</string>
<string name="routing_settings_import_rulesets">وارد کردن مجموعه قوانین</string>
<string name="routing_settings_import_rulesets_tip">مجموعه قوانین موجود حذف خواهند شد، آیا مطمئن هستید که ادامه می دهید؟</string>
<string name="routing_settings_import_rulesets_from_clipboard">وارد کردن مجموعه قوانین از کلیپ بورد</string>
<string name="routing_settings_export_rulesets_to_clipboard">صادر کردن مجموعه قوانین به کلیپ بورد</string>
<string name="routing_settings_locked">قفل است، این قانون را هنگام وارد کردن از پیش تنظیم‌ها حفظ کنید</string>
<string name="connection_test_pending">اتصال را بررسی کنید</string>
<string name="connection_test_testing">در حال آزمایش...</string>
@@ -298,4 +302,11 @@
<item>Dark</item>
</string-array>
<string-array name="preset_rulesets">
<item>لیست سفید چین</item>
<item>لیست سیاه چین</item>
<item>جهانی(Global)</item>
<item>ایران</item>
</string-array>
</resources>

View File

@@ -97,6 +97,7 @@
<string name="server_lab_content">Данные</string>
<string name="toast_none_data_clipboard">В буфере обмена нет данных</string>
<string name="toast_invalid_url">Неправильный URL</string>
<string name="toast_insecure_url_protocol">Please do not use the insecure HTTP protocol subscription address</string>
<string name="server_lab_need_inbound">Убедитесь, что входящий порт соответствует настройкам</string>
<string name="toast_malformed_josn">Профиль повреждён</string>
<string name="server_lab_request_host6">Узел (SNI) (необязательно)</string>
@@ -110,7 +111,7 @@
<string name="msg_file_not_found">Файл не найден</string>
<string name="msg_remark_is_duplicate">Название уже существует</string>
<string name="toast_action_not_allowed">Это действие запрещено</string>
<string name="server_obfs_password">Пароль obfs</string>
<!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">Загрузка…</string>
@@ -130,8 +131,8 @@
<string name="title_vpn_settings">Настройки VPN</string>
<string name="title_pref_per_app_proxy">Прокси для выбранных приложений</string>
<string name="summary_pref_per_app_proxy">Основной: выделенное приложение соединяется через прокси, не выделенное — напрямую;\nРежим обхода: выделенное приложение соединяется напрямую, не выделенное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню.</string>
<string name="title_pref_is_booted">Auto connect at startup</string>
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
<string name="title_pref_is_booted">Автоподключение при запуске</string>
<string name="summary_pref_is_booted">Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным)</string>
<string name="title_mux_settings">Настройки мультиплексирования</string>
<string name="title_pref_mux_enabled">Использовать мультиплексирование</string>
@@ -235,10 +236,10 @@
<string name="sub_setting_url">URL (необязательно)</string>
<string name="sub_setting_filter">Название фильтра</string>
<string name="sub_setting_enable">Использовать обновление</string>
<string name="sub_auto_update">Использовать автоматическое обновление</string>
<string name="sub_auto_update">Использовать автообновление</string>
<string name="sub_setting_pre_profile">Название предыдущего прокси</string>
<string name="sub_setting_next_profile">Название следующего прокси</string>
<string name="sub_setting_pre_profile_tip">Убедитесь, что название существует и является уникальным</string>
<string name="sub_setting_pre_profile_tip">Название должно существовать и быть уникальным</string>
<string name="title_sub_update">Обновить подписку группы</string>
<string name="title_ping_all_server">Проверка профилей группы</string>
<string name="title_real_ping_all_server">Время отклика профилей группы</string>
@@ -253,14 +254,16 @@
<string name="routing_settings_domain_strategy">Доменная стратегия</string>
<string name="routing_settings_title">Маршрутизация</string>
<string name="routing_settings_tips">Введите требуемые значения через запятую</string>
<string name="routing_settings_tips">Введите требуемые домены/IP через запятую</string>
<string name="routing_settings_save">Сохранить</string>
<string name="routing_settings_delete">Очистить</string>
<string name="routing_settings_rule_title">Настройка правил маршрутизации</string>
<string name="routing_settings_add_rule">Добавить правило</string>
<string name="routing_settings_import_rulesets">Импорт правил</string>
<string name="routing_settings_import_rulesets_tip">Существующие правила будут удалены. Продолжить?</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="routing_settings_import_rulesets_from_clipboard">Импорт правил из буфера обмена</string>
<string name="routing_settings_export_rulesets_to_clipboard">Экспорт правил в буфер обмена</string>
<string name="routing_settings_locked">Постоянное (сохранится при импорте правил)</string>
<string name="routing_settings_domain">Домен</string>
<string name="routing_settings_ip">IP</string>
<string name="routing_settings_port">Порт</string>

View File

@@ -97,6 +97,7 @@
<string name="server_lab_content">Nội dung</string>
<string name="toast_none_data_clipboard">Không có dữ liệu nào trong Clipboard!</string>
<string name="toast_invalid_url">URL không hợp lệ hoặc trống!</string>
<string name="toast_insecure_url_protocol">Please do not use the insecure HTTP protocol subscription address</string>
<string name="server_lab_need_inbound">Vui lòng đảm bảo cấu hình tùy chỉnh này không bị lỗi trước khi sử dụng!</string>
<string name="toast_malformed_josn">Cấu hình không hợp lệ!</string>
<string name="server_lab_request_host6">Host (SNI) (Không bắt buộc)</string>
@@ -105,6 +106,7 @@
<string name="title_url">URL</string>
<string name="menu_item_download_file">Tải xuống tệp tin</string>
<string name="toast_action_not_allowed">Hành động này bị cấm!</string>
<string name="server_obfs_password">Obfs password</string>
<!-- PerAppProxyActivity -->
<string name="title_user_asset_add_url">Thêm URL nội dung</string>
@@ -259,6 +261,8 @@
<string name="routing_settings_add_rule">Add rule</string>
<string name="routing_settings_import_rulesets">Import ruleset</string>
<string name="routing_settings_import_rulesets_tip">Existing rulesets will be deleted, are you sure to continue?</string>
<string name="routing_settings_import_rulesets_from_clipboard">Import ruleset from clipboard</string>
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="connection_test_pending">Kiểm tra kết nối</string>

View File

@@ -97,6 +97,7 @@
<string name="server_lab_content">内容</string>
<string name="toast_none_data_clipboard">剪贴板中没有数据</string>
<string name="toast_invalid_url">无效的网址</string>
<string name="toast_insecure_url_protocol">请不要使用不安全的HTTP协议订阅地址</string>
<string name="server_lab_need_inbound">确保inbounds port和设置中的一致</string>
<string name="toast_malformed_josn">配置格式错误</string>
<string name="server_lab_request_host6">Host(SNI)(可选)</string>
@@ -105,7 +106,7 @@
<string name="title_url">URL</string>
<string name="menu_item_download_file">下载文件</string>
<string name="toast_action_not_allowed">禁止此项操作</string>
<string name="server_obfs_password">混淆密码</string>
<!-- PerAppProxyActivity -->
<string name="title_user_asset_add_url">添加资产网址</string>
@@ -257,6 +258,8 @@
<string name="routing_settings_add_rule">添加规则</string>
<string name="routing_settings_import_rulesets">导入预设规则集</string>
<string name="routing_settings_import_rulesets_tip">将删除现有的规则集,是否确定继续?</string>
<string name="routing_settings_import_rulesets_from_clipboard">从剪贴板导入规则集</string>
<string name="routing_settings_export_rulesets_to_clipboard">导出规则集至剪贴板</string>
<string name="routing_settings_locked">锁定中,导入预设时不删除此规则</string>
<string name="connection_test_pending">"检查网络连接"</string>
@@ -304,6 +307,7 @@
<item>绕过大陆(Whitelist)</item>
<item>黑名单(Blacklist)</item>
<item>全局(Global)</item>
<item>伊朗(Iran)</item>
</string-array>
</resources>

View File

@@ -18,12 +18,12 @@
<string name="toast_services_failure">啟動服務失敗</string>
<!--ServerActivity-->
<string name="title_server">配置檔案</string>
<string name="menu_item_add_config">新增配置</string>
<string name="menu_item_save_config">儲存配置</string>
<string name="menu_item_del_config">刪除配置</string>
<string name="menu_item_import_config_qrcode">從 QR Code 匯入配置</string>
<string name="menu_item_import_config_clipboard">從剪貼簿匯入配置</string>
<string name="title_server">設定檔</string>
<string name="menu_item_add_config">新增設定</string>
<string name="menu_item_save_config">儲存設定</string>
<string name="menu_item_del_config">刪除設定</string>
<string name="menu_item_import_config_qrcode">從 QR Code 匯入設定</string>
<string name="menu_item_import_config_clipboard">從剪貼簿匯入設定</string>
<string name="menu_item_import_config_manually_vmess">手動鍵入 [VMess]</string>
<string name="menu_item_import_config_manually_vless">手動鍵入 [VLESS]</string>
<string name="menu_item_import_config_manually_ss">手動鍵入 [Shadowsocks]</string>
@@ -32,11 +32,11 @@
<string name="menu_item_import_config_manually_trojan">手動鍵入 [Trojan]</string>
<string name="menu_item_import_config_manually_wireguard">手動鍵入 [Wireguard]</string>
<string name="menu_item_import_config_manually_hysteria2">手動鍵入 [Hysteria2]</string>
<string name="menu_item_import_config_custom">自訂配置</string>
<string name="menu_item_import_config_custom_clipboard">從剪貼簿匯入自訂配置</string>
<string name="menu_item_import_config_custom_local">從本地匯入自訂配置</string>
<string name="menu_item_import_config_custom_url">從 URL 匯入自訂配置</string>
<string name="menu_item_import_config_custom_url_scan">掃描 URL 匯入自訂配置</string>
<string name="menu_item_import_config_custom">自訂設定</string>
<string name="menu_item_import_config_custom_clipboard">從剪貼簿匯入自訂設定</string>
<string name="menu_item_import_config_custom_local">從本地匯入自訂設定</string>
<string name="menu_item_import_config_custom_url">從 URL 匯入自訂設定</string>
<string name="menu_item_import_config_custom_url_scan">掃描 URL 匯入自訂設定</string>
<string name="del_config_comfirm">確定刪除?</string>
<string name="del_invalid_config_comfirm">刪除前請先測試!確認刪除?</string>
<string name="server_lab_remarks">備註</string>
@@ -90,21 +90,23 @@
<string name="toast_none_data">無資料</string>
<string name="toast_incorrect_protocol">通訊協定不正確</string>
<string name="toast_decoding_failed">解碼失敗</string>
<string name="title_file_chooser">選取一個配置</string>
<string name="title_file_chooser">選取一個設定</string>
<string name="toast_require_file_manager">請安裝檔案總管。</string>
<string name="server_customize_config">自訂配置</string>
<string name="toast_config_file_invalid">無效配置</string>
<string name="server_customize_config">自訂設定</string>
<string name="toast_config_file_invalid">無效設定</string>
<string name="server_lab_content">內容</string>
<string name="toast_none_data_clipboard">剪貼簿內無資料</string>
<string name="toast_invalid_url">URL 無效</string>
<string name="toast_insecure_url_protocol">請不要使用不安全的HTTP協定訂閱位址</string>
<string name="server_lab_need_inbound">​​確保 inbounds port 和設定中的一致</string>
<string name="toast_malformed_josn">配置格式不正確</string>
<string name="toast_malformed_josn">設定格式不正確</string>
<string name="server_lab_request_host6">Host(SNI)(可選)</string>
<string name="toast_asset_copy_failed">失敗,請使用檔案總管</string>
<string name="menu_item_add_file">新增檔案</string>
<string name="title_url">URL</string>
<string name="menu_item_download_file">下載檔案</string>
<string name="toast_action_not_allowed">禁止此項操作</string>
<string name="server_obfs_password">混淆密碼</string>
<!-- PerAppProxyActivity -->
<string name="title_user_asset_add_url">新增資產網址</string>
@@ -187,8 +189,8 @@
<string name="title_pref_local_dns_port">本機 DNS 埠</string>
<string name="summary_pref_local_dns_port">本機 DNS 埠</string>
<string name="title_pref_confirm_remove">刪除配置檔案確認</string>
<string name="summary_pref_confirm_remove">刪除配置檔案是否需要用戶二次確認</string>
<string name="title_pref_confirm_remove">刪除設定檔確認</string>
<string name="summary_pref_confirm_remove">刪除設定檔是否需要用戶二次確認</string>
<string name="title_pref_start_scan_immediate">立即啟動掃碼</string>
<string name="summary_pref_start_scan_immediate">啟動時立即打開相機掃描,否則可在工具欄選擇掃碼或選照片</string>
@@ -201,10 +203,10 @@
<string name="title_about">關於</string>
<string name="title_source_code">原始碼</string>
<string name="title_tg_channel">Telegram 頻道</string>
<string name="title_configuration_backup">備份配置</string>
<string name="title_configuration_backup">備份設定</string>
<string name="summary_configuration_backup">儲存位置: [%s], 卸載App或清除儲存後備份將被清除</string>
<string name="title_configuration_restore">還原配置</string>
<string name="title_configuration_share">分享配置</string>
<string name="title_configuration_restore">還原設定</string>
<string name="title_configuration_share">分享設定</string>
<string name="title_pref_promotion">推廣</string>
<string name="summary_pref_promotion">一些推廣,輕觸以檢視 (捐贈可去除)</string>
@@ -224,10 +226,10 @@
<string name="logcat_copy">複製</string>
<string name="logcat_clear">清除</string>
<string name="title_service_restart">重啟服務</string>
<string name="title_del_all_config">刪除目前群組配置</string>
<string name="title_del_duplicate_config">刪除目前群組重複配置</string>
<string name="title_del_invalid_config">刪除目前群組無效配置</string>
<string name="title_export_all">匯出目前群組配置至剪貼簿</string>
<string name="title_del_all_config">刪除目前群組設定</string>
<string name="title_del_duplicate_config">刪除目前群組重複設定</string>
<string name="title_del_invalid_config">刪除目前群組無效設定</string>
<string name="title_export_all">匯出目前群組設定至剪貼簿</string>
<string name="title_sub_setting">訂閱分組設定</string>
<string name="sub_setting_remarks">備註</string>
<string name="sub_setting_url">可選位址(url)</string>
@@ -238,11 +240,11 @@
<string name="sub_setting_next_profile">落地代理別名</string>
<string name="sub_setting_pre_profile_tip">请确保别名存在并唯一</string>
<string name="title_sub_update">更新目前群組訂閱</string>
<string name="title_ping_all_server">偵測目前群組配置 Tcping</string>
<string name="title_real_ping_all_server">偵測目前群組配置真延遲</string>
<string name="title_ping_all_server">偵測目前群組設定 Tcping</string>
<string name="title_real_ping_all_server">偵測目前群組設定真延遲</string>
<string name="title_user_asset_setting">Geo 資源檔案</string>
<string name="title_sort_by_test_results">依偵測結果排序</string>
<string name="title_filter_config">過濾配置</string>
<string name="title_filter_config">過濾設定</string>
<string name="filter_config_all">所有分組</string>
<string name="title_del_duplicate_config_count">Delete %d duplicate configurations</string>
@@ -258,6 +260,8 @@
<string name="routing_settings_add_rule">新增規則</string>
<string name="routing_settings_import_rulesets">匯入預設規則集</string>
<string name="routing_settings_import_rulesets_tip">將刪除現有的規則集,是否確定繼續? </string>
<string name="routing_settings_import_rulesets_from_clipboard">從剪貼簿匯入規則集</string>
<string name="routing_settings_export_rulesets_to_clipboard">匯出規則集至剪貼簿</string>
<string name="routing_settings_locked">鎖定中,匯入預設時不刪除此規則</string>
<string name="connection_test_pending">"測試連線能力"</string>
@@ -282,7 +286,7 @@
<string-array name="share_method">
<item>QR Code</item>
<item>匯出至剪貼簿</item>
<item>匯出完整配置至剪貼簿</item>
<item>匯出完整設定至剪貼簿</item>
</string-array>
<string-array name="share_sub_method">
@@ -305,6 +309,7 @@
<item>繞過大陸(Whitelist)</item>
<item>黑名單(Blacklist)</item>
<item>全域(Global)</item>
<item>伊朗(Iran)</item>
</string-array>
</resources>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="TabLayoutTextStyle" parent="TextAppearance.Design.Tab">
<item name="textAllCaps">false</item>
</style>

View File

@@ -98,6 +98,7 @@
<string name="server_lab_content">Content</string>
<string name="toast_none_data_clipboard">There is no data in the clipboard</string>
<string name="toast_invalid_url">Invalid URL</string>
<string name="toast_insecure_url_protocol">Please do not use the insecure HTTP protocol subscription address</string>
<string name="server_lab_need_inbound">Ensure inbounds port is consistent with the settings</string>
<string name="toast_malformed_josn">Config malformed</string>
<string name="server_lab_request_host6">Host(SNI)(Optional)</string>
@@ -111,7 +112,7 @@
<string name="msg_file_not_found">File not found</string>
<string name="msg_remark_is_duplicate">The remarks already exists</string>
<string name="toast_action_not_allowed">Action not allowed</string>
<string name="server_obfs_password">Obfs password</string>
<!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">Loading</string>
@@ -263,6 +264,8 @@
<string name="routing_settings_add_rule">Add rule</string>
<string name="routing_settings_import_rulesets">Import ruleset</string>
<string name="routing_settings_import_rulesets_tip">Existing rulesets will be deleted, are you sure to continue?</string>
<string name="routing_settings_import_rulesets_from_clipboard">Import ruleset from clipboard</string>
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="routing_settings_domain" translatable="false">domain</string>
<string name="routing_settings_ip" translatable="false">ip</string>
@@ -316,6 +319,7 @@
<item>China Whitelist</item>
<item>China Blacklist</item>
<item>Global</item>
<item>Iran Whitelist</item>
</string-array>
</resources>

View File

@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.4.2" apply false
id("com.android.library") version "8.4.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.23" apply false
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.android.kotlin) apply false
}

View File

@@ -1,18 +1,18 @@
[versions]
activityKtx = "1.9.2"
activityKtx = "1.9.3"
appcompat = "1.7.0"
cardview = "1.0.0"
constraintlayout = "2.1.4"
core = "3.5.3"
editorkit = "2.9.0"
flexbox = "3.0.0"
fragmentKtx = "1.8.3"
fragmentKtx = "1.8.4"
gson = "2.11.0"
junit = "4.13.2"
kotlinReflect = "2.0.20"
kotlinReflect = "2.0.21"
kotlinxCoroutinesCore = "1.9.0"
legacySupportV4 = "1.0.0"
lifecycleViewmodelKtx = "2.8.5"
lifecycleViewmodelKtx = "2.8.6"
material = "1.12.0"
mmkvStatic = "1.3.9"
multidex = "2.0.1"
@@ -25,6 +25,8 @@ rxpermissions = "0.12"
toastcompat = "1.1.0"
viewpager2 = "1.1.0"
workRuntimeKtx = "2.9.1"
androidGradlePlugin = "8.7.1"
androidKotlinPlugin = "2.0.21"
[libraries]
activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
@@ -60,4 +62,7 @@ viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpag
work-multiprocess = { module = "androidx.work:work-multiprocess", version.ref = "workRuntimeKtx" }
work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }
[plugins]
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
android-kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "androidKotlinPlugin" }