Compare commits

...

130 Commits

Author SHA1 Message Date
2dust
9a9d315e62 up 1.8.39 2024-08-30 19:12:51 +08:00
2dust
c42aa93bf7 Add UDP noise
002d08bf83
2024-08-30 19:11:51 +08:00
Tamim Hossain
a7664f03aa Update Kotlin Version In Readme.md (#3525)
Update Kotlin Version In Readme.md
2024-08-30 14:53:41 +08:00
Tamim Hossain
fa341c9a5a Added wireguard regular config parsing feature, which we discussed on issue #3497 (#3521) 2024-08-29 19:31:44 +08:00
Tamim Hossain
a15ab4759e Close the service tag correctly (#3519) 2024-08-29 19:29:40 +08:00
2dust
b8939763d4 up 1.8.38 2024-08-17 19:46:20 +08:00
2dust
51adca8568 Format code 2024-08-17 19:40:04 +08:00
Tamim Hossain
f646eff048 Removed internet check (#3494)
Removed the internet connection check before connecting as per the request in issue #3486
2024-08-17 14:59:21 +08:00
2dust
fee0a016d8 Optimized search 2024-08-17 14:58:48 +08:00
2dust
f040fa5c08 Bug fix
https://github.com/2dust/v2rayNG/issues/3488
2024-08-16 18:12:53 +08:00
2dust
7f3c6b4665 Bug fix 2024-08-16 17:59:36 +08:00
Tamim Hossain
8b806fe0be Fix logcat flush blocking call on the wrong dispatcher (#3491)
* Fix logcat flush blocking call on the wrong dispatcher

Moved the blocking `process.waitFor()` call to `Dispatchers.IO` to avoid potential thread starvation on `Dispatchers.Default`. This change ensures that the I/O-bound operation does not interfere with CPU-bound tasks, improving overall performance and responsiveness.

* Fix logcat flush blocking call on the wrong dispatcher

Moved the blocking `process.waitFor()` call to `Dispatchers.IO` to avoid potential thread starvation on `Dispatchers.Default`. This change ensures that the I/O-bound operation does not interfere with CPU-bound tasks, improving overall performance and responsiveness.
2024-08-16 17:50:04 +08:00
Tamim Hossain
b37d8c2369 Refactor null handling with .orEmpty() for better readability (#3490)
Replaced null handling using `?: ""` with `.orEmpty()` for improved readability. The `.orEmpty()` extension function provides a more concise and expressive way to handle null strings by returning an empty string if the original string is null. This change enhances code clarity and consistency.
2024-08-16 17:49:29 +08:00
Tamim Hossain
b5451e9d3d Update getSerializableExtra usage for Android 13 or later (#3489)
Adjusted the usage of `getSerializableExtra` to handle its deprecation in Android 13 (API level 33) and later. Implemented the method requiring class type specification for retrieving `Pair<String, Long>` in newer API levels, while maintaining compatibility with older versions using the legacy approach. This change ensures proper functionality across all supported Android versions.
2024-08-16 17:46:13 +08:00
Tamim Hossain
b2235d4c38 Fix issue #3475 (#3485)
Fix issue where the label in the tile and widget was not displaying as `v2rayNG` for Russian language users
2024-08-16 10:09:53 +08:00
Tamim Hossain
c8ba5d727e Updated WorkManager (#3483)
* Updated WorkManager

Updated the WorkManager library to latest version and also updated the code for its initialization.

* Updated WorkManager

Updated the WorkManager library to latest version and also updated the code for its initialization.
2024-08-15 17:07:25 +08:00
solokot
a3de44cd0a Update Russian translation (#3482) 2024-08-15 17:06:11 +08:00
2dust
214d9e1c53 up 1.8.37 2024-08-15 09:58:44 +08:00
2dust
92900c3f74 Optimize text descriptions 2024-08-15 09:54:42 +08:00
2dust
e17e566daa Update subscriptions based on grouping
https://github.com/2dust/v2rayNG/issues/3445
2024-08-14 20:16:26 +08:00
2dust
df4e232087 Code Optimization 2024-08-14 19:58:32 +08:00
2dust
828a39331b Add a progress bar 2024-08-14 17:55:41 +08:00
2dust
a0223a3eee logic optimization
https://github.com/2dust/v2rayNG/issues/3470
2024-08-14 15:06:21 +08:00
2dust
7f6a526b25 Add profile filtering
https://github.com/2dust/v2rayNG/issues/3471
2024-08-14 10:46:42 +08:00
2dust
9983ea25d2 Performance optimization 2024-08-13 21:17:27 +08:00
2dust
47166b937f Main interface list performance optimization 2024-08-13 17:07:07 +08:00
2dust
0a09966e81 Update UserAssetActivity.kt 2024-08-13 16:09:53 +08:00
2dust
7ec34e934e Bug fix 2024-08-13 15:11:53 +08:00
2dust
f2b03e7492 Merge branch 'master' of https://github.com/2dust/v2rayNG 2024-08-13 15:10:34 +08:00
2dust
5208bd62c5 Optimized UI
com.google.android.material.progressindicator.LinearProgressIndicator
2024-08-13 14:39:45 +08:00
AmirMohammad Yazdanmanesh
d9f0854c27 Fix some Persian word translations (#3469)
* Translate fingerprint

* The word 'موفقیت' is a noun and does not mean 'success' in this context

* Translate 'about'
2024-08-12 19:35:50 +08:00
Tamim Hossain
6be125b5cb Replace deprecated packagingOptions with packaging for jniLibs configuration (#3464)
Replaced deprecated packagingOptions with packaging for jniLibs configuration.

Updated build.gradle file to use the new packaging configuration method as the previous
packagingOptions method is deprecated. This change ensures compatibility with the latest
Gradle plugin versions and avoids any potential issues with outdated configuration.

Changes:
- Updated from `packagingOptions { jniLibs { useLegacyPackaging = true } }`
  to `packaging { jniLibs { useLegacyPackaging = true } }`
2024-08-12 09:20:23 +08:00
2dust
c884c098fd Bug fix
https://github.com/2dust/v2rayNG/issues/3426
2024-08-10 20:11:34 +08:00
AnGgIt86
f77fe05c92 Update RxPermissions and rxjava3 (#3463) 2024-08-10 19:56:37 +08:00
Tamim Hossain
f3bfa8ceba Update null safety handling with orEmpty() (#3461)
Replaced ?: "" with orEmpty() for improved null safety

Updated all instances of the null-coalescing operator (?: "") with orEmpty() to enhance null safety across the codebase. This change ensures that an empty string is used when a nullable value is null, improving consistency and reducing potential null-related issues.
2024-08-10 19:54:45 +08:00
Tamim Hossain
ae7d9d87d2 Inline return statements for cleaner code (#3460)
Refactored conditional return statements to be inline.
- Simplified code readability by inlining return statements.
- Maintained functionality while improving code clarity.
2024-08-10 19:53:35 +08:00
Tamim Hossain
1652040c1c Simplify search logic using SearchView (#3459)
Refactor the search functionality to use SearchView for better handling of search queries.
- Removed redundant code and replaced it with SearchView's onQueryTextChange listener.
- Improved readability and maintainability of the search logic.
2024-08-10 19:51:45 +08:00
Tamim Hossain
818b7cdff4 Replace global scope with CoroutineScope for socket operations (#3454)
Replaced the use of global scope with CoroutineScope(Dispatchers.IO) in the socket operation function. This change improves coroutine management and ensures proper use of dispatchers for background tasks. The code now properly handles retries with exponential backoff and logs attempts and errors.
2024-08-10 19:48:53 +08:00
Tamim Hossain
48ce359d2d Fix: Refactor to use lazy initialization for binding (#3453)
Refactor binding initialization to use lazy delegation

Replaced direct initialization of binding with lazy initialization to improve performance and resource management. This change defers the creation of the binding object until it is actually needed, which can reduce memory usage and improve efficiency.

- Updated binding initialization to use lazy
- Ensured proper access and lifecycle handling

This update ensures that the binding is initialized only when required, leading to a more efficient use of resources.
2024-08-10 19:41:37 +08:00
Tamim Hossain
e115bf0c6d Refactor EConfigType to use constants from AppConfig (#3451)
Updated the EConfigType enum to use protocol scheme constants from AppConfig. This change improves maintainability by centralizing protocol scheme definitions in a single location.
2024-08-10 19:34:24 +08:00
Tamim Hossain
0415b60ba5 Fix ABI split configuration for updated Gradle plugin API (#3449)
* Fix ABI split configuration for updated Gradle plugin API

Updated the ABI split configuration in the build.gradle file to use the new syntax.
Replaced deprecated `reset()` method with the updated `splits.abi` configuration block and added `universalApk = true` to include a universal APK.

* Unresolved reference: universalApk

Unresolved reference: universalApk
2024-08-10 19:32:32 +08:00
AmirMohammad Yazdanmanesh
4570fdb05f Implement a check to start V2Ray if the network connected (#3439)
* Implement isNetworkConnected functionality

* Implement a check to start V2Ray if the network connected
2024-08-10 19:23:52 +08:00
mayampi01
9d109e7ca9 Hardcode popular Android Private DNS rule to fix localhost DNS problem (#3433)
* hardcode popular Android Private DNS rule to fix localhost DNS problem

* V2rayConfigUtil.kt: Fix hosts type

* hardcode popular Android Private DNS: Add dns.pub
2024-08-10 19:21:47 +08:00
2dust
2574553180 up 1.8.36 2024-08-05 20:28:36 +08:00
2dust
66e77d50bd Optimized UI 2024-08-04 19:18:16 +08:00
mayampi01
253bd793d7 Add geosite:private and make it work (#3418)
* Add geosite:private and make it work

* Update AppConfig.kt

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2024-08-04 10:15:55 +08:00
2dust
be30de6728 Update build.yml 2024-08-03 10:44:12 +08:00
Tamim Hossain
a1455bbb1c Updated getLocale function to use appropriate Locale constants (#3413)
Updated getLocale function to use appropriate Locale constants
2024-08-03 10:38:00 +08:00
Tamim Hossain
1ac19ae3e9 Fix getDarkModeStatus to handle configuration changes more accurately (#3412)
Updated getDarkModeStatus function to improve detection of dark mode status.
2024-08-03 10:36:21 +08:00
Tamim Hossain
6e6ca209df Refactor base64 decode function to simplify handling (#3411)
Refactored the decode function to simplify base64 decoding and improve handling of various base64 formats. Removed redundant code and improved clarity.
2024-08-03 10:35:15 +08:00
Tamim Hossain
52699967cd Simplify parseInt function (#3410)
Refactored the parseInt function to use toIntOrNull() for more concise and idiomatic error handling. This change simplifies the code and avoids the need for manual exception handling
2024-08-03 10:33:28 +08:00
Tamim Hossain
146d20ce86 Optimize strState Handling by Removing Redundant try-catch (#3409)
Refactored the code for handling the `strState` string to remove the redundant `try-catch` block. Updated to use safe call with the Elvis operator for improved readability and efficiency. This change simplifies the code and reduces the need for exception handling in this context.
2024-08-03 10:30:38 +08:00
Tamim Hossain
b6959b5990 Refactor filterConfig Function for Simplified Code and Reusability (#3408)
Refactored the `filterConfig` function in `MainViewModel` to simplify the code and improve reusability. The updated implementation reduces redundancy, enhances readability, and streamlines the configuration filtering process, making the code more efficient and maintainable.
2024-08-03 10:20:28 +08:00
Tamim Hossain
06649df8b1 Add Documentation Comments to AppConfig Constants (#3406)
This branch adds comprehensive documentation comments to the AppConfig object in the codebase
2024-08-03 09:52:00 +08:00
Tamim Hossain
0174ed9082 Remove redundant access modifier (#3405)
Removed unnecessary access modifier
2024-08-03 09:49:44 +08:00
2dust
adabb281b1 Update string 2024-08-02 10:11:49 +08:00
2dust
63e710d1ab Simplifying the filter configuration file 2024-08-02 10:05:40 +08:00
Tamim Hossain
509a568446 Added Bangla Translation (#3399)
Added Bangla translations for all resources in the project. The translations were provided by a native Bangla speaker from Bangladesh—myself! This update includes translations for all project elements to support localization for Bangla-speaking users.
2024-08-01 20:18:51 +08:00
Tamim Hossain
6fd94b53f0 Refactor charset usage and consolidate function implementation (#3398)
- Updated charset usage from string literal "UTF_8" to standard `Charsets.UTF_8`.
- Consolidated the implementation of the `getDelayTestUrl()` function.
- Improved code readability and consistency with these changes.
2024-08-01 20:17:02 +08:00
Tamim Hossain
164412fa34 Refactor GlobalScope Usage to Prevent Memory Leaks (#3396)
Replaced usage of GlobalScope with a specific coroutine scope tied to the lifecycle of the service . This change helps to prevent potential memory leaks and unintended behavior by ensuring coroutines are properly managed and tied to the appropriate lifecycle.
2024-08-01 20:07:30 +08:00
Tamim Hossain
514ca0810e Refactor V2RayServiceManager to Use Constants for Configuration Values (#3395)
Refactor V2RayServiceManager to Use Constants for Configuration Values.
- Replaced hardcoded string literals for 'uplink' and 'downlink' with constants from AppConfig.
- Introduced constants for notification channel ID and name.
2024-08-01 20:05:09 +08:00
Tamim Hossain
7582f86482 Refactor extension functions for better performance and clarity (#3394)
Refactor Kotlin extension functions for clarity and performance

- Updated `v2RayApplication` extension to handle potential ClassCastException.
- Simplified `toast` functions for better readability.
- Refactored `toTrafficString` function to reduce redundancy and improve performance.
- Minor improvements to JSON and URL connection handling.
2024-08-01 19:44:07 +08:00
mayampi01
4b4c46e5ae Drop geolocation-cn (cn contains geolocation-cn) (#3393) 2024-08-01 19:38:08 +08:00
2dust
162e156b33 Bug fix
https://github.com/2dust/v2rayNG/pull/3390
2024-08-01 09:47:33 +08:00
mayampi01
bc7d1971ef Add localhost DNS support (#3384) 2024-08-01 09:27:46 +08:00
2dust
bbdee92f37 Determine custom rule order based on preset rules
https://github.com/2dust/v2rayNG/pull/3388
2024-07-31 21:01:16 +08:00
Tamim Hossain
cf9e830cc7 Replace deprecated onBackPressed with onBackPressedDispatcher (#3389)
Replaced deprecated `onBackPressed` with `onBackPressedDispatcher`. The `onBackPressedDispatcher` handles the home button press by delegating to the appropriate back navigation method.
2024-07-31 20:36:52 +08:00
mayampi01
804e425a87 Make IPIfNonMatch actually work (#3387) 2024-07-31 20:18:40 +08:00
mayampi01
cc21383928 Fix typo: s/Domian/Domain/g (#3383) 2024-07-31 20:15:06 +08:00
2dust
413f4efd69 up 1.8.35 2024-07-29 19:12:58 +08:00
Tamim Hossain
de69605eff Introduced version catalog for streamlined dependency management (#3377)
This pull request integrates a version catalog (libs.versions.toml) to centralize dependency management within the project. By utilizing this approach, we enhance dependency consistency, reduce maintenance overhead, and facilitate version updates across multiple modules. This change provides a more organized and efficient way to manage project dependencies.
2024-07-28 09:27:58 +08:00
mayampi01
9338ba3525 Set UseIP in Direct to break the Android Private DNS dead loop (#3362)
* Set UseIP in Direct to break the Android Private DNS dead loop

* fakedns: No need to set UseIP again
2024-07-25 19:50:23 +08:00
2dust
322b6ec615 Bug fix 2024-07-25 19:46:39 +08:00
2dust
304232d029 up 1.8.34 2024-07-22 11:34:44 +08:00
2dust
f80c3bfe07 up 1.8.33 2024-07-20 15:05:45 +08:00
2dust
bb0a62fc8b up 1.8.32 2024-07-18 10:45:05 +08:00
2dust
5bfdca6cd9 Bug fix 2024-07-18 10:15:58 +08:00
2dust
447e712a9d up 1.8.31 2024-07-16 14:05:34 +08:00
2dust
8bb03f189d up 1.8.30 2024-07-13 10:43:46 +08:00
2dust
3b0554cd9b up 1.8.29 2024-07-12 13:30:38 +08:00
2dust
858101b0d9 Bug fix
https://github.com/2dust/v2rayNG/issues/3234
https://github.com/2dust/v2rayNG/issues/3291
2024-07-11 16:41:38 +08:00
2dust
a7cf8bee28 Code clean
Add allowInsecure to Share
2024-07-11 16:40:25 +08:00
2dust
af1ec7bea9 Bug fix 2024-07-09 12:29:04 +08:00
2dust
002bf7ef22 up 1.8.28 2024-07-07 18:14:36 +08:00
2dust
6919e2336d Bug fix
https://github.com/2dust/v2rayNG/issues/3278
2024-07-01 18:09:57 +08:00
2dust
5a5bd22073 Bug fix
https://github.com/2dust/v2rayNG/issues/3232
2024-07-01 10:34:45 +08:00
2dust
a726f00f35 Bug fix 2024-06-29 16:47:06 +08:00
2dust
79297f8a42 up 1.8.27
App bundle support
2024-06-29 15:32:01 +08:00
2dust
e3f70ac253 Add LEANBACK_LAUNCHER 2024-06-29 15:29:20 +08:00
GFW-knocker
838b346fcc fix wireguard issue (#3260)
fix crash when invalid wireguard config imported
2024-06-28 09:41:55 +08:00
solokot
eba9545ccf Update Russian translation to 1.8.26 (#3238) 2024-06-24 09:09:35 +08:00
2dust
890ade9495 up 1.8.26 2024-06-21 20:33:34 +08:00
2dust
518ef1e0ec Add splithttp transport for xray 2024-06-21 20:18:04 +08:00
2dust
bdcecfca72 Hardcode gstatic.com taking detour [proxy] 2024-06-21 16:48:30 +08:00
2dust
1363846ac4 Fix view 2024-06-08 20:24:23 +08:00
2dust
30347546a2 Optimized code 2024-06-08 10:17:54 +08:00
2dust
ac7eb28e91 Auto update subscriptions when adding a url 2024-06-07 21:06:27 +08:00
2dust
748405473b Fix
https://github.com/2dust/v2rayNG/issues/3175
2024-06-07 10:51:13 +08:00
2dust
1080390bed Adding a second test when a delayed test fails
https://www.google.com/generate_204
2024-06-07 10:25:23 +08:00
2dust
c48725c7dd Bug fix
https://github.com/2dust/v2rayNG/issues/3193
2024-06-06 19:48:50 +08:00
2dust
a5287dbadc Optimized code 2024-06-02 20:50:27 +08:00
solokot
ee5a3b0dd9 Update Russian translation (#3172) 2024-05-30 20:06:16 +08:00
2dust
3d001541e5 Refactor the parser 2024-05-30 20:04:43 +08:00
2dust
b376b229b9 Add progress bar when subscription is updated 2024-05-30 16:07:11 +08:00
2dust
33b6203978 Bug fix 2024-05-29 20:30:10 +08:00
2dust
2d803e009c Bug fix
https://github.com/2dust/v2rayNG/issues/3168
2024-05-29 17:41:58 +08:00
2dust
2ddbe38781 Merge branch 'master' of https://github.com/2dust/v2rayNG 2024-05-25 20:32:37 +08:00
user09283
96181a2b8d Update strings.xml (#3152)
Update Vietnamese language!
2024-05-24 20:42:30 +08:00
2dust
8308b8eaf2 Bug fix
android.app.RemoteServiceException$ForegroundServiceDidNotStartInTimeException: Context.startForegroundService() did not then call Service.startForeground(): ServiceRecord{8f445a6 u0 com.v2ray.ang/.service.V2RayVpnService}
2024-05-24 10:35:46 +08:00
2dust
00f26ff529 up 1.8.25 2024-05-23 10:54:47 +08:00
2dust
49dcdf3ae5 used resources 2024-05-22 17:41:55 +08:00
2dust
409b431d1c unused resources 2024-05-20 17:12:54 +08:00
2dust
6da988e3db up 1.8.24 2024-05-18 13:45:44 +08:00
2dust
fd8f8306ee up dependency 2024-05-18 13:44:43 +08:00
2dust
74b342f5c6 Bug fix
https://github.com/2dust/v2rayNG/issues/3126
2024-05-17 14:04:15 +08:00
2dust
2504ec79ee Change delayed test URL
https://github.com/2dust/v2rayNG/issues/3110
2024-05-14 10:53:15 +08:00
2dust
3e7b211b17 Bug fix
https://github.com/2dust/v2rayNG/issues/3106
2024-05-12 16:47:27 +08:00
solokot
13d5514a4c Update Russian translation (#3098) 2024-05-12 11:55:57 +08:00
2dust
f0f9da0f1b Add delayed test URL 2024-05-12 11:55:10 +08:00
2dust
f6d2c5f473 up 1.8.23 2024-05-08 17:18:17 +08:00
2dust
6e8dd5b250 Update strings.xml 2024-05-08 15:34:14 +08:00
2dust
f4779bc50c Update strings.xml 2024-05-08 15:33:00 +08:00
ibrahem Qasim
432baf262d Update Arabic translation (#3089)
* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml
2024-05-07 09:42:15 +08:00
2dust
a1d68fcde3 Added attempt to update subscription via proxy
https://github.com/2dust/v2rayNG/issues/2760
2024-05-05 17:08:05 +08:00
2dust
e053db3dff Add routeOnly 2024-05-02 10:51:54 +08:00
solokot
f624bd651e Update Russian translation (#3070) 2024-05-01 19:37:03 +08:00
2dust
84964c7f91 Add attributes to grpc
https://github.com/2dust/v2rayNG/issues/3041
2024-05-01 14:01:34 +08:00
2dust
96f56b468e Adjust DNS hardcoded geoip:cn issue
https://github.com/2dust/v2rayNG/issues/3061
2024-05-01 14:00:35 +08:00
2dust
bdc3212f38 Rule start with ext and contains geoip added to the ips
https://github.com/2dust/v2rayNG/issues/3060
2024-05-01 10:43:47 +08:00
2dust
508ddf6df2 Share configuration 2024-04-28 19:47:54 +08:00
145 changed files with 4595 additions and 2986 deletions

View File

@@ -47,7 +47,7 @@ jobs:
go get github.com/xtls/xray-core@${{ github.event.inputs.XRAY_CORE_VERSION }} || true go get github.com/xtls/xray-core@${{ github.event.inputs.XRAY_CORE_VERSION }} || true
gomobile init gomobile init
go mod tidy -v go mod tidy -v
gomobile bind -v -androidapi 19 -ldflags='-s -w' ./ gomobile bind -v -androidapi 21 -ldflags='-s -w' ./
cp *.aar ${{ github.workspace }}/V2rayNG/app/libs/ cp *.aar ${{ github.workspace }}/V2rayNG/app/libs/
- name: Build APK - name: Build 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) 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) [![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.6.21-blue.svg)](https://kotlinlang.org) [![Kotlin Version](https://img.shields.io/badge/Kotlin-1.9.23-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) [![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) [![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) [![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayNG/latest/total?logo=github)](https://github.com/2dust/v2rayNG/releases)

View File

@@ -11,9 +11,22 @@ android {
applicationId = "com.v2ray.ang" applicationId = "com.v2ray.ang"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 558 versionCode = 585
versionName = "1.8.22" versionName = "1.8.39"
multiDexEnabled = true multiDexEnabled = true
splits {
abi {
isEnable = true
include(
"arm64-v8a",
"armeabi-v7a",
"x86_64",
"x86"
)
isUniversalApk = true
}
}
} }
compileOptions { compileOptions {
@@ -41,17 +54,10 @@ android {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
} }
splits {
abi {
isEnable = true
isUniversalApk = true
}
}
applicationVariants.all { applicationVariants.all {
val variant = this val variant = this
val versionCodes = val versionCodes =
mapOf("armeabi-v7a" to 1, "arm64-v8a" to 2, "x86" to 3, "x86_64" to 4) mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4)
variant.outputs variant.outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl } .map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
@@ -59,15 +65,12 @@ android {
val abi = if (output.getFilter("ABI") != null) val abi = if (output.getFilter("ABI") != null)
output.getFilter("ABI") output.getFilter("ABI")
else else
"all" "universal"
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk" output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
if(versionCodes.containsKey(abi)) if (versionCodes.containsKey(abi)) {
{
output.versionCodeOverride = (1000000 * versionCodes[abi]!!).plus(variant.versionCode) output.versionCodeOverride = (1000000 * versionCodes[abi]!!).plus(variant.versionCode)
} } else {
else
{
return@forEach return@forEach
} }
} }
@@ -77,47 +80,53 @@ android {
viewBinding = true viewBinding = true
buildConfig = true buildConfig = true
} }
packaging {
jniLibs {
useLegacyPackaging = true
}
}
} }
dependencies { dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar","*.jar")))) implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
testImplementation("junit:junit:4.13.2") testImplementation(libs.junit)
implementation(libs.flexbox)
// Androidx // Androidx
implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation(libs.constraintlayout)
implementation("androidx.legacy:legacy-support-v4:1.0.0") implementation(libs.legacy.support.v4)
implementation("androidx.appcompat:appcompat:1.6.1") implementation(libs.appcompat)
implementation("com.google.android.material:material:1.11.0") implementation(libs.material)
implementation("androidx.cardview:cardview:1.0.0") implementation(libs.cardview)
implementation("androidx.preference:preference-ktx:1.2.1") implementation(libs.preference.ktx)
implementation("androidx.recyclerview:recyclerview:1.3.2") implementation(libs.recyclerview)
implementation("androidx.fragment:fragment-ktx:1.6.2") implementation(libs.fragment.ktx)
implementation("androidx.multidex:multidex:2.0.1") implementation(libs.multidex)
implementation("androidx.viewpager2:viewpager2:1.1.0-beta02") implementation(libs.viewpager2)
// Androidx ktx // Androidx ktx
implementation("androidx.activity:activity-ktx:1.9.0") implementation(libs.activity.ktx)
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") implementation(libs.lifecycle.viewmodel.ktx)
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") implementation(libs.lifecycle.livedata.ktx)
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") implementation(libs.lifecycle.runtime.ktx)
//kotlin //kotlin
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.23") implementation(libs.kotlin.reflect)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") implementation(libs.kotlinx.coroutines.core)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0") implementation(libs.kotlinx.coroutines.android)
implementation("com.tencent:mmkv-static:1.3.4") implementation(libs.mmkv.static)
implementation("com.google.code.gson:gson:2.10.1") implementation(libs.gson)
implementation("io.reactivex:rxjava:1.3.8") implementation(libs.rxjava)
implementation("io.reactivex:rxandroid:1.2.1") implementation(libs.rxandroid)
implementation("com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar") implementation(libs.rxpermissions)
implementation("me.drakeet.support:toastcompat:1.1.0") implementation(libs.toastcompat)
implementation("com.blacksquircle.ui:editorkit:2.9.0") implementation(libs.editorkit)
implementation("com.blacksquircle.ui:language-base:2.9.0") implementation(libs.language.base)
implementation("com.blacksquircle.ui:language-json:2.9.0") implementation(libs.language.json)
implementation("io.github.g00fy2.quickie:quickie-bundled:1.9.0") implementation(libs.quickie.bundled)
implementation("com.google.zxing:core:3.5.3") implementation(libs.core)
implementation(libs.work.runtime.ktx)
implementation("androidx.work:work-runtime-ktx:2.8.1") implementation(libs.work.multiprocess)
implementation("androidx.work:work-multiprocess:2.8.1")
} }

View File

@@ -8,17 +8,28 @@
android:smallScreens="true" android:smallScreens="true"
android:normalScreens="true" android:normalScreens="true"
android:largeScreens="true" android:largeScreens="true"
android:xlargeScreens="true"/> android:xlargeScreens="true" />
<uses-sdk android:minSdkVersion="21" tools:overrideLibrary="com.blacksquircle.ui.editorkit"/> <uses-sdk
android:minSdkVersion="21"
tools:overrideLibrary="com.blacksquircle.ui.editorkit" />
<uses-feature android:name="android.hardware.camera" android:required="false"/> <uses-feature
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/> android:name="android.hardware.camera"
<uses-feature android:name="android.software.leanback" android:required="false" /> android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" /> <uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<!-- https://developer.android.com/about/versions/11/privacy/package-visibility --> <!-- https://developer.android.com/about/versions/11/privacy/package-visibility -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" <uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
@@ -31,7 +42,7 @@
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
android:minSdkVersion="34" /> android:minSdkVersion="34" />
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> --> <!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
@@ -53,12 +64,14 @@
android:theme="@style/AppThemeDayNight.NoActionBar"> android:theme="@style/AppThemeDayNight.NoActionBar">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<!-- <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>--> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" /> <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />
@@ -119,12 +132,14 @@
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="v2rayng"/> <category android:name="android.intent.category.DEFAULT" />
<data android:host="install-config"/>
<data android:host="install-sub"/> <data android:scheme="v2rayng" />
<data android:host="install-config" />
<data android:host="install-sub" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
@@ -150,7 +165,8 @@
android:value="vpn" /> android:value="vpn" />
</service> </service>
<service android:name=".service.V2RayProxyOnlyService" <service
android:name=".service.V2RayProxyOnlyService"
android:exported="false" android:exported="false"
android:label="@string/app_name" android:label="@string/app_name"
android:foregroundServiceType="specialUse" android:foregroundServiceType="specialUse"
@@ -160,10 +176,11 @@
android:value="proxy" /> android:value="proxy" />
</service> </service>
<service android:name=".service.V2RayTestService" <service
android:name=".service.V2RayTestService"
android:exported="false" android:exported="false"
android:process=":RunSoLibV2RayDaemon"> android:process=":RunSoLibV2RayDaemon"
</service> />
<receiver <receiver
android:exported="true" android:exported="true"
@@ -227,6 +244,16 @@
</provider> </provider>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.cache"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/cache_paths" />
</provider>
</application> </application>
</manifest> </manifest>

View File

@@ -1,2 +1 @@
geosite:cn, geosite:cn
geosite:geolocation-cn

View File

@@ -81,7 +81,9 @@
}, },
{ {
"protocol": "freedom", "protocol": "freedom",
"settings": {}, "settings": {
"domainStrategy": "UseIP"
},
"tag": "direct" "tag": "direct"
}, },
{ {

View File

@@ -16,8 +16,8 @@
package com.v2ray.ang.helper; package com.v2ray.ang.helper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
/** /**
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}. * Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
@@ -36,7 +36,6 @@ public interface ItemTouchHelperAdapter {
* @param fromPosition The start position of the moved item. * @param fromPosition The start position of the moved item.
* @param toPosition Then resolved position of the moved item. * @param toPosition Then resolved position of the moved item.
* @return True if the item was moved to the new adapter position. * @return True if the item was moved to the new adapter position.
*
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
* @see RecyclerView.ViewHolder#getAdapterPosition() * @see RecyclerView.ViewHolder#getAdapterPosition()
*/ */
@@ -52,7 +51,6 @@ public interface ItemTouchHelperAdapter {
* adjusting the underlying data to reflect this removal. * adjusting the underlying data to reflect this removal.
* *
* @param position The position of the item dismissed. * @param position The position of the item dismissed.
*
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
* @see RecyclerView.ViewHolder#getAdapterPosition() * @see RecyclerView.ViewHolder#getAdapterPosition()
*/ */

View File

@@ -17,9 +17,10 @@
package com.v2ray.ang.helper; package com.v2ray.ang.helper;
import android.graphics.Canvas; import android.graphics.Canvas;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;

View File

@@ -3,10 +3,11 @@ package com.v2ray.ang
import android.content.Context import android.content.Context
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import androidx.work.Configuration import androidx.work.Configuration
import androidx.work.WorkManager
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
class AngApplication : MultiDexApplication(), Configuration.Provider { class AngApplication : MultiDexApplication() {
companion object { companion object {
//const val PREF_LAST_VERSION = "pref_last_version" //const val PREF_LAST_VERSION = "pref_last_version"
lateinit var application: AngApplication lateinit var application: AngApplication
@@ -17,8 +18,9 @@ class AngApplication : MultiDexApplication(), Configuration.Provider {
application = this application = this
} }
//var firstRun = false private val workManagerConfiguration: Configuration = Configuration.Builder()
// private set .setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
.build()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@@ -34,11 +36,7 @@ class AngApplication : MultiDexApplication(), Configuration.Provider {
MMKV.initialize(this) MMKV.initialize(this)
Utils.setNightMode(application) Utils.setNightMode(application)
} // Initialize WorkManager with the custom configuration
WorkManager.initialize(this, workManagerConfiguration)
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder()
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
.build()
} }
} }

View File

@@ -1,20 +1,22 @@
package com.v2ray.ang package com.v2ray.ang
/**
*
* App Config Const
*/
object AppConfig { object AppConfig {
/** The application's package name. */
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
/** Directory names used in the app's file system. */
const val DIR_ASSETS = "assets" const val DIR_ASSETS = "assets"
const val DIR_BACKUPS = "backups" const val DIR_BACKUPS = "backups"
// legacy /** Legacy configuration keys. */
const val ANG_CONFIG = "ang_config" const val ANG_CONFIG = "ang_config"
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium" const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
// Preferences mapped to MMKV /** Preferences mapped to MMKV storage. */
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled" const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
const val PREF_ROUTE_ONLY_ENABLED = "pref_route_only_enabled"
const val PREF_PER_APP_PROXY = "pref_per_app_proxy" const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set" const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
const val PREF_BYPASS_APPS = "pref_bypass_apps" const val PREF_BYPASS_APPS = "pref_bypass_apps"
@@ -22,69 +24,73 @@ object AppConfig {
const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled" const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port" const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
const val PREF_VPN_DNS = "pref_vpn_dns" const val PREF_VPN_DNS = "pref_vpn_dns"
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy" const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
const val PREF_ROUTING_MODE = "pref_routing_mode" const val PREF_ROUTING_MODE = "pref_routing_mode"
const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent" const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent"
const val PREF_V2RAY_ROUTING_DIRECT = "pref_v2ray_routing_direct" const val PREF_V2RAY_ROUTING_DIRECT = "pref_v2ray_routing_direct"
const val PREF_V2RAY_ROUTING_BLOCKED = "pref_v2ray_routing_blocked" const val PREF_V2RAY_ROUTING_BLOCKED = "pref_v2ray_routing_blocked"
const val PREF_ROUTING_CUSTOM = "pref_routing_custom" const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
const val PREF_MUX_ENABLED = "pref_mux_enabled" const val PREF_MUX_ENABLED = "pref_mux_enabled"
const val PREF_MUX_CONCURRENCY = "pref_mux_concurency" const val PREF_MUX_CONCURRENCY = "pref_mux_concurrency"
const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurency" const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurrency"
const val PREF_MUX_XUDP_QUIC = "pref_mux_xudp_quic" const val PREF_MUX_XUDP_QUIC = "pref_mux_xudp_quic"
const val PREF_FRAGMENT_ENABLED = "pref_fragment_enabled" const val PREF_FRAGMENT_ENABLED = "pref_fragment_enabled"
const val PREF_FRAGMENT_PACKETS = "pref_fragment_packets" const val PREF_FRAGMENT_PACKETS = "pref_fragment_packets"
const val PREF_FRAGMENT_LENGTH = "pref_fragment_length" const val PREF_FRAGMENT_LENGTH = "pref_fragment_length"
const val PREF_FRAGMENT_INTERVAL = "pref_fragment_interval" const val PREF_FRAGMENT_INTERVAL = "pref_fragment_interval"
const val SUBSCRIPTION_AUTO_UPDATE = "pref_auto_update_subscription" const val SUBSCRIPTION_AUTO_UPDATE = "pref_auto_update_subscription"
const val SUBSCRIPTION_AUTO_UPDATE_INTERVAL = "pref_auto_update_interval" const val SUBSCRIPTION_AUTO_UPDATE_INTERVAL = "pref_auto_update_interval"
const val SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL = "1440" // 24 hours const val SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL = "1440" // Default is 24 hours
const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater" const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater"
const val PREF_SPEED_ENABLED = "pref_speed_enabled" const val PREF_SPEED_ENABLED = "pref_speed_enabled"
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove" const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate" const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
const val PREF_LANGUAGE = "pref_language" const val PREF_LANGUAGE = "pref_language"
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night" const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6" const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled" const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
const val PREF_ALLOW_INSECURE = "pref_allow_insecure" const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
const val PREF_SOCKS_PORT = "pref_socks_port" const val PREF_SOCKS_PORT = "pref_socks_port"
const val PREF_HTTP_PORT = "pref_http_port" const val PREF_HTTP_PORT = "pref_http_port"
const val PREF_REMOTE_DNS = "pref_remote_dns" const val PREF_REMOTE_DNS = "pref_remote_dns"
const val PREF_DOMESTIC_DNS = "pref_domestic_dns" const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
const val PREF_LOGLEVEL = "pref_core_loglevel" const val PREF_LOGLEVEL = "pref_core_loglevel"
const val PREF_MODE = "pref_mode" const val PREF_MODE = "pref_mode"
/** Cache keys. */
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id" const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter" const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
//Preferences mapped to MMKV End /** Protocol identifiers. */
const val PROTOCOL_HTTP: String = "http://" const val PROTOCOL_HTTP: String = "http://"
const val PROTOCOL_HTTPS: String = "https://" const val PROTOCOL_HTTPS: String = "https://"
const val PROTOCOL_FREEDOM: String = "freedom" const val PROTOCOL_FREEDOM: String = "freedom"
/** Broadcast actions. */
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service" const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity" const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click" const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click"
/** Tasker extras. */
const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE" const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
const val TASKER_EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB" const val TASKER_EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"
const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch" const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch"
const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid" const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
const val TASKER_DEFAULT_GUID = "Default" const val TASKER_DEFAULT_GUID = "Default"
/** Tags for different proxy modes. */
const val TAG_PROXY = "proxy" const val TAG_PROXY = "proxy"
const val TAG_DIRECT = "direct" const val TAG_DIRECT = "direct"
const val TAG_BLOCKED = "block" const val TAG_BLOCKED = "block"
const val TAG_FRAGMENT = "fragment" const val TAG_FRAGMENT = "fragment"
/** Network-related constants. */
const val UPLINK = "uplink"
const val DOWNLINK = "downlink"
/** URLs for various resources. */
const val androidpackagenamelistUrl = const val androidpackagenamelistUrl =
"https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt" "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
const val v2rayCustomRoutingListUrl = const val v2rayCustomRoutingListUrl =
@@ -96,11 +102,15 @@ object AppConfig {
const val PromotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw=" const val PromotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/" const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/"
const val TgChannelUrl = "https://t.me/github_2dust" const val TgChannelUrl = "https://t.me/github_2dust"
const val DelayTestUrl = "https://www.gstatic.com/generate_204"
const val DelayTestUrl2 = "https://www.google.com/generate_204"
/** DNS server addresses. */
const val DNS_PROXY = "1.1.1.1" const val DNS_PROXY = "1.1.1.1"
const val DNS_DIRECT = "223.5.5.5" const val DNS_DIRECT = "223.5.5.5"
const val DNS_VPN = "1.1.1.1" const val DNS_VPN = "1.1.1.1"
/** Ports and addresses for various services. */
const val PORT_LOCAL_DNS = "10853" const val PORT_LOCAL_DNS = "10853"
const val PORT_SOCKS = "10808" const val PORT_SOCKS = "10808"
const val PORT_HTTP = "10809" const val PORT_HTTP = "10809"
@@ -108,6 +118,7 @@ object AppConfig {
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128" const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
const val WIREGUARD_LOCAL_MTU = "1420" const val WIREGUARD_LOCAL_MTU = "1420"
/** Message constants for communication. */
const val MSG_REGISTER_CLIENT = 1 const val MSG_REGISTER_CLIENT = 1
const val MSG_STATE_RUNNING = 11 const val MSG_STATE_RUNNING = 11
const val MSG_STATE_NOT_RUNNING = 12 const val MSG_STATE_NOT_RUNNING = 12
@@ -123,4 +134,19 @@ object AppConfig {
const val MSG_MEASURE_CONFIG = 7 const val MSG_MEASURE_CONFIG = 7
const val MSG_MEASURE_CONFIG_SUCCESS = 71 const val MSG_MEASURE_CONFIG_SUCCESS = 71
const val MSG_MEASURE_CONFIG_CANCEL = 72 const val MSG_MEASURE_CONFIG_CANCEL = 72
/** Notification channel IDs and names. */
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
const val RAY_NG_CHANNEL_NAME = "V2rayNG Background Service"
const val SUBSCRIPTION_UPDATE_CHANNEL = "subscription_update_channel"
const val SUBSCRIPTION_UPDATE_CHANNEL_NAME = "Subscription Update Service"
/** Protocols Scheme **/
const val VMESS = "vmess://"
const val CUSTOM = ""
const val SHADOWSOCKS = "ss://"
const val SOCKS = "socks://"
const val VLESS = "vless://"
const val TROJAN = "trojan://"
const val WIREGUARD = "wireguard://"
} }

View File

@@ -1,32 +0,0 @@
package com.v2ray.ang.dto
data class AngConfig(
var index: Int,
var vmess: ArrayList<VmessBean>,
var subItem: ArrayList<SubItemBean>
) {
data class VmessBean(var guid: String = "123456",
var address: String = "v2ray.cool",
var port: Int = 10086,
var id: String = "a3482e88-686a-4a58-8126-99c9df64b7bf",
var alterId: Int = 64,
var security: String = "aes-128-cfb",
var network: String = "tcp",
var remarks: String = "def",
var headerType: String = "",
var requestHost: String = "",
var path: String = "",
var streamSecurity: String = "",
var allowInsecure: String = "",
var configType: Int = 1,
var configVersion: Int = 1,
var testResult: String = "",
var subid: String = "",
var flow: String = "",
var sni: String = "")
data class SubItemBean(var id: String = "",
var remarks: String = "",
var url: String = "",
var enabled: Boolean = true)
}

View File

@@ -2,8 +2,10 @@ package com.v2ray.ang.dto
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
data class AppInfo(val appName: String, data class AppInfo(
val appName: String,
val packageName: String, val packageName: String,
val appIcon: Drawable, val appIcon: Drawable,
val isSystemApp: Boolean, val isSystemApp: Boolean,
var isSelected: Int) var isSelected: Int
)

View File

@@ -1,13 +1,16 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto
import com.v2ray.ang.AppConfig
enum class EConfigType(val value: Int, val protocolScheme: String) { enum class EConfigType(val value: Int, val protocolScheme: String) {
VMESS(1, "vmess://"), VMESS(1, AppConfig.VMESS),
CUSTOM(2, ""), CUSTOM(2, AppConfig.CUSTOM),
SHADOWSOCKS(3, "ss://"), SHADOWSOCKS(3, AppConfig.SHADOWSOCKS),
SOCKS(4, "socks://"), SOCKS(4, AppConfig.SOCKS),
VLESS(5, "vless://"), VLESS(5, AppConfig.VLESS),
TROJAN(6, "trojan://"), TROJAN(6, AppConfig.TROJAN),
WIREGUARD(7, "wireguard://"); WIREGUARD(7, AppConfig.WIREGUARD);
companion object { companion object {
fun fromInt(value: Int) = values().firstOrNull { it.value == value } fun fromInt(value: Int) = values().firstOrNull { it.value == value }

View File

@@ -1,6 +1,6 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto
enum class ERoutingMode(val value: String ) { enum class ERoutingMode(val value: String) {
GLOBAL_PROXY("0"), GLOBAL_PROXY("0"),
BYPASS_LAN("1"), BYPASS_LAN("1"),
BYPASS_MAINLAND("2"), BYPASS_MAINLAND("2"),

View File

@@ -0,0 +1,9 @@
package com.v2ray.ang.dto
data class ProfileItem(
val configType: EConfigType,
var subscriptionId: String = "",
var remarks: String = "",
var server: String?,
var serverPort: Int?,
)

View File

@@ -1,8 +1,8 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.AppConfig.TAG_BLOCKED import com.v2ray.ang.AppConfig.TAG_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
data class ServerConfig( data class ServerConfig(
@@ -16,26 +16,38 @@ data class ServerConfig(
) { ) {
companion object { companion object {
fun create(configType: EConfigType): ServerConfig { fun create(configType: EConfigType): ServerConfig {
when(configType) { when (configType) {
EConfigType.VMESS, EConfigType.VLESS -> EConfigType.VMESS, EConfigType.VLESS ->
return ServerConfig( return ServerConfig(
configType = configType, configType = configType,
outboundBean = V2rayConfig.OutboundBean( outboundBean = V2rayConfig.OutboundBean(
protocol = configType.name.lowercase(), protocol = configType.name.lowercase(),
settings = V2rayConfig.OutboundBean.OutSettingsBean( settings = V2rayConfig.OutboundBean.OutSettingsBean(
vnext = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean( vnext = listOf(
users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())))), V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean())) users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())
)
)
),
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()
)
)
EConfigType.CUSTOM -> EConfigType.CUSTOM ->
return ServerConfig(configType = configType) return ServerConfig(configType = configType)
EConfigType.SHADOWSOCKS, EConfigType.SOCKS, EConfigType.TROJAN -> EConfigType.SHADOWSOCKS, EConfigType.SOCKS, EConfigType.TROJAN ->
return ServerConfig( return ServerConfig(
configType = configType, configType = configType,
outboundBean = V2rayConfig.OutboundBean( outboundBean = V2rayConfig.OutboundBean(
protocol = configType.name.lowercase(), protocol = configType.name.lowercase(),
settings = V2rayConfig.OutboundBean.OutSettingsBean( settings = V2rayConfig.OutboundBean.OutSettingsBean(
servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())), servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean())) ),
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()
)
)
EConfigType.WIREGUARD -> EConfigType.WIREGUARD ->
return ServerConfig( return ServerConfig(
configType = configType, configType = configType,
@@ -44,7 +56,9 @@ data class ServerConfig(
settings = V2rayConfig.OutboundBean.OutSettingsBean( settings = V2rayConfig.OutboundBean.OutSettingsBean(
secretKey = "", secretKey = "",
peers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean()) peers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean())
))) )
)
)
} }
} }
} }

View File

@@ -1,4 +1,6 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto
data class ServersCache(val guid: String, data class ServersCache(
val config: ServerConfig) val guid: String,
val profile: ProfileItem
)

View File

@@ -24,7 +24,8 @@ data class V2rayConfig(
var fakedns: Any? = null, var fakedns: Any? = null,
val browserForwarder: Any? = null, val browserForwarder: Any? = null,
var observatory: Any? = null, var observatory: Any? = null,
var burstObservatory: Any? = null) { var burstObservatory: Any? = null
) {
companion object { companion object {
const val DEFAULT_PORT = 443 const val DEFAULT_PORT = 443
const val DEFAULT_SECURITY = "auto" const val DEFAULT_SECURITY = "auto"
@@ -36,10 +37,12 @@ data class V2rayConfig(
const val HTTP = "http" const val HTTP = "http"
} }
data class LogBean(val access: String, data class LogBean(
val access: String,
val error: String, val error: String,
var loglevel: String?, var loglevel: String?,
val dnsLog: Boolean? = null) val dnsLog: Boolean? = null
)
data class InboundBean( data class InboundBean(
var tag: String, var tag: String,
@@ -49,30 +52,40 @@ data class V2rayConfig(
val settings: Any? = null, val settings: Any? = null,
val sniffing: SniffingBean?, val sniffing: SniffingBean?,
val streamSettings: Any? = null, val streamSettings: Any? = null,
val allocate: Any? = null) { val allocate: Any? = null
) {
data class InSettingsBean(val auth: String? = null, data class InSettingsBean(
val auth: String? = null,
val udp: Boolean? = null, val udp: Boolean? = null,
val userLevel: Int? = null, val userLevel: Int? = null,
val address: String? = null, val address: String? = null,
val port: Int? = null, val port: Int? = null,
val network: String? = null) val network: String? = null
)
data class SniffingBean(var enabled: Boolean, data class SniffingBean(
var enabled: Boolean,
val destOverride: ArrayList<String>, val destOverride: ArrayList<String>,
val metadataOnly: Boolean? = null) val metadataOnly: Boolean? = null,
var routeOnly: Boolean? = null
)
} }
data class OutboundBean(var tag: String = "proxy", data class OutboundBean(
var tag: String = "proxy",
var protocol: String, var protocol: String,
var settings: OutSettingsBean? = null, var settings: OutSettingsBean? = null,
var streamSettings: StreamSettingsBean? = null, var streamSettings: StreamSettingsBean? = null,
val proxySettings: Any? = null, val proxySettings: Any? = null,
val sendThrough: String? = null, val sendThrough: String? = null,
var mux: MuxBean? = MuxBean(false)) { var mux: MuxBean? = MuxBean(false)
) {
data class OutSettingsBean(var vnext: List<VnextBean>? = null, data class OutSettingsBean(
var vnext: List<VnextBean>? = null,
var fragment: FragmentBean? = null, var fragment: FragmentBean? = null,
var noise: NoiseBean? = null,
var servers: List<ServersBean>? = null, var servers: List<ServersBean>? = null,
/*Blackhole*/ /*Blackhole*/
var response: Response? = null, var response: Response? = null,
@@ -90,26 +103,38 @@ data class V2rayConfig(
var secretKey: String? = null, var secretKey: String? = null,
val peers: List<WireGuardBean>? = null, val peers: List<WireGuardBean>? = null,
var reserved: List<Int>? = null, var reserved: List<Int>? = null,
var mtu :Int? = null var mtu: Int? = null
) { ) {
data class VnextBean(var address: String = "", data class VnextBean(
var address: String = "",
var port: Int = DEFAULT_PORT, var port: Int = DEFAULT_PORT,
var users: List<UsersBean>) { var users: List<UsersBean>
) {
data class UsersBean(var id: String = "", data class UsersBean(
var id: String = "",
var alterId: Int? = null, var alterId: Int? = null,
var security: String = DEFAULT_SECURITY, var security: String = DEFAULT_SECURITY,
var level: Int = DEFAULT_LEVEL, var level: Int = DEFAULT_LEVEL,
var encryption: String = "", var encryption: String = "",
var flow: String = "") var flow: String = ""
)
} }
data class FragmentBean(var packets: String? = null, data class FragmentBean(
var packets: String? = null,
var length: String? = null, var length: String? = null,
var interval: String? = null) var interval: String? = null
)
data class ServersBean(var address: String = "", data class NoiseBean(
var packet: String? = null,
var delay: String? = null
)
data class ServersBean(
var address: String = "",
var method: String = "chacha20-poly1305", var method: String = "chacha20-poly1305",
var ota: Boolean = false, var ota: Boolean = false,
var password: String = "", var password: String = "",
@@ -118,26 +143,33 @@ data class V2rayConfig(
val email: String? = null, val email: String? = null,
var flow: String? = null, var flow: String? = null,
val ivCheck: Boolean? = null, val ivCheck: Boolean? = null,
var users: List<SocksUsersBean>? = null) { var users: List<SocksUsersBean>? = null
) {
data class SocksUsersBean(var user: String = "", data class SocksUsersBean(
var user: String = "",
var pass: String = "", var pass: String = "",
var level: Int = DEFAULT_LEVEL) var level: Int = DEFAULT_LEVEL
)
} }
data class Response(var type: String) data class Response(var type: String)
data class WireGuardBean(var publicKey: String = "", data class WireGuardBean(
var endpoint: String = "") var publicKey: String = "",
var endpoint: String = ""
)
} }
data class StreamSettingsBean(var network: String = DEFAULT_NETWORK, data class StreamSettingsBean(
var network: String = DEFAULT_NETWORK,
var security: String = "", var security: String = "",
var tcpSettings: TcpSettingsBean? = null, var tcpSettings: TcpSettingsBean? = null,
var kcpSettings: KcpSettingsBean? = null, var kcpSettings: KcpSettingsBean? = null,
var wsSettings: WsSettingsBean? = null, var wsSettings: WsSettingsBean? = null,
var httpupgradeSettings: HttpupgradeSettingsBean? = null, var httpupgradeSettings: HttpupgradeSettingsBean? = null,
var splithttpSettings: SplithttpSettingsBean? = null,
var httpSettings: HttpSettingsBean? = null, var httpSettings: HttpSettingsBean? = null,
var tlsSettings: TlsSettingsBean? = null, var tlsSettings: TlsSettingsBean? = null,
var quicSettings: QuicSettingBean? = null, var quicSettings: QuicSettingBean? = null,
@@ -147,27 +179,36 @@ data class V2rayConfig(
var sockopt: SockoptBean? = null var sockopt: SockoptBean? = null
) { ) {
data class TcpSettingsBean(var header: HeaderBean = HeaderBean(), data class TcpSettingsBean(
val acceptProxyProtocol: Boolean? = null) { var header: HeaderBean = HeaderBean(),
data class HeaderBean(var type: String = "none", val acceptProxyProtocol: Boolean? = null
) {
data class HeaderBean(
var type: String = "none",
var request: RequestBean? = null, var request: RequestBean? = null,
var response: Any? = null) { var response: Any? = null
data class RequestBean(var path: List<String> = ArrayList(), ) {
data class RequestBean(
var path: List<String> = ArrayList(),
var headers: HeadersBean = HeadersBean(), var headers: HeadersBean = HeadersBean(),
val version: String? = null, val version: String? = null,
val method: String? = null) { val method: String? = null
data class HeadersBean(var Host: List<String> = ArrayList(), ) {
data class HeadersBean(
var Host: List<String>? = ArrayList(),
@SerializedName("User-Agent") @SerializedName("User-Agent")
val userAgent: List<String>? = null, val userAgent: List<String>? = null,
@SerializedName("Accept-Encoding") @SerializedName("Accept-Encoding")
val acceptEncoding: List<String>? = null, val acceptEncoding: List<String>? = null,
val Connection: List<String>? = null, val Connection: List<String>? = null,
val Pragma: String? = null) val Pragma: String? = null
)
} }
} }
} }
data class KcpSettingsBean(var mtu: Int = 1350, data class KcpSettingsBean(
var mtu: Int = 1350,
var tti: Int = 50, var tti: Int = 50,
var uplinkCapacity: Int = 12, var uplinkCapacity: Int = 12,
var downlinkCapacity: Int = 100, var downlinkCapacity: Int = 100,
@@ -175,33 +216,50 @@ data class V2rayConfig(
var readBufferSize: Int = 1, var readBufferSize: Int = 1,
var writeBufferSize: Int = 1, var writeBufferSize: Int = 1,
var header: HeaderBean = HeaderBean(), var header: HeaderBean = HeaderBean(),
var seed: String? = null) { var seed: String? = null
) {
data class HeaderBean(var type: String = "none") data class HeaderBean(var type: String = "none")
} }
data class WsSettingsBean(var path: String = "", data class WsSettingsBean(
var path: String = "",
var headers: HeadersBean = HeadersBean(), var headers: HeadersBean = HeadersBean(),
val maxEarlyData: Int? = null, val maxEarlyData: Int? = null,
val useBrowserForwarding: Boolean? = null, val useBrowserForwarding: Boolean? = null,
val acceptProxyProtocol: Boolean? = null) { val acceptProxyProtocol: Boolean? = null
) {
data class HeadersBean(var Host: String = "") data class HeadersBean(var Host: String = "")
} }
data class HttpupgradeSettingsBean(var path: String = "", data class HttpupgradeSettingsBean(
var path: String = "",
var host: String = "", var host: String = "",
val acceptProxyProtocol: Boolean? = null) val acceptProxyProtocol: Boolean? = null
)
data class HttpSettingsBean(var host: List<String> = ArrayList(), data class SplithttpSettingsBean(
var path: String = "") var path: String = "",
var host: String = "",
val maxUploadSize: Int? = null,
val maxConcurrentUploads: Int? = null
)
data class SockoptBean(var TcpNoDelay: Boolean? = null, data class HttpSettingsBean(
var host: List<String> = ArrayList(),
var path: String = ""
)
data class SockoptBean(
var TcpNoDelay: Boolean? = null,
var tcpKeepAliveIdle: Int? = null, var tcpKeepAliveIdle: Int? = null,
var tcpFastOpen: Boolean? = null, var tcpFastOpen: Boolean? = null,
var tproxy: String? = null, var tproxy: String? = null,
var mark: Int? = null, var mark: Int? = null,
var dialerProxy: String? = null) var dialerProxy: String? = null
)
data class TlsSettingsBean(var allowInsecure: Boolean = false, data class TlsSettingsBean(
var allowInsecure: Boolean = false,
var serverName: String = "", var serverName: String = "",
val alpn: List<String>? = null, val alpn: List<String>? = null,
val minVersion: String? = null, val minVersion: String? = null,
@@ -216,21 +274,30 @@ data class V2rayConfig(
val show: Boolean = false, val show: Boolean = false,
var publicKey: String? = null, var publicKey: String? = null,
var shortId: String? = null, var shortId: String? = null,
var spiderX: String? = null) var spiderX: String? = null
)
data class QuicSettingBean(var security: String = "none", data class QuicSettingBean(
var security: String = "none",
var key: String = "", var key: String = "",
var header: HeaderBean = HeaderBean()) { var header: HeaderBean = HeaderBean()
) {
data class HeaderBean(var type: String = "none") data class HeaderBean(var type: String = "none")
} }
data class GrpcSettingsBean(var serviceName: String = "", data class GrpcSettingsBean(
var serviceName: String = "",
var authority: String? = null, var authority: String? = null,
var multiMode: Boolean? = null) var multiMode: Boolean? = null,
var idle_timeout: Int? = null,
var health_check_timeout: Int? = null
)
fun populateTransportSettings(transport: String, headerType: String?, host: String?, path: String?, seed: String?, fun populateTransportSettings(
transport: String, headerType: String?, host: String?, path: String?, seed: String?,
quicSecurity: String?, key: String?, mode: String?, serviceName: String?, quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
authority: String?): String { authority: String?
): String {
var sni = "" var sni = ""
network = transport network = transport
when (network) { when (network) {
@@ -240,17 +307,18 @@ data class V2rayConfig(
tcpSetting.header.type = HTTP tcpSetting.header.type = HTTP
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) { if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
val requestObj = TcpSettingsBean.HeaderBean.RequestBean() val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
requestObj.headers.Host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() } requestObj.headers.Host = (host.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() }
requestObj.path = (path ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() } requestObj.path = (path.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() }
tcpSetting.header.request = requestObj tcpSetting.header.request = requestObj
sni = requestObj.headers.Host.getOrNull(0) ?: sni sni = requestObj.headers.Host?.getOrNull(0) ?: sni
} }
} else { } else {
tcpSetting.header.type = "none" tcpSetting.header.type = "none"
sni = host ?: "" sni = host.orEmpty()
} }
tcpSettings = tcpSetting tcpSettings = tcpSetting
} }
"kcp" -> { "kcp" -> {
val kcpsetting = KcpSettingsBean() val kcpsetting = KcpSettingsBean()
kcpsetting.header.type = headerType ?: "none" kcpsetting.header.type = headerType ?: "none"
@@ -261,49 +329,66 @@ data class V2rayConfig(
} }
kcpSettings = kcpsetting kcpSettings = kcpsetting
} }
"ws" -> { "ws" -> {
val wssetting = WsSettingsBean() val wssetting = WsSettingsBean()
wssetting.headers.Host = host ?: "" wssetting.headers.Host = host.orEmpty()
sni = wssetting.headers.Host sni = wssetting.headers.Host
wssetting.path = path ?: "/" wssetting.path = path ?: "/"
wsSettings = wssetting wsSettings = wssetting
} }
"httpupgrade" -> { "httpupgrade" -> {
val httpupgradeSetting = HttpupgradeSettingsBean() val httpupgradeSetting = HttpupgradeSettingsBean()
httpupgradeSetting.host = host ?: "" httpupgradeSetting.host = host.orEmpty()
sni = httpupgradeSetting.host sni = httpupgradeSetting.host
httpupgradeSetting.path = path ?: "/" httpupgradeSetting.path = path ?: "/"
httpupgradeSettings = httpupgradeSetting httpupgradeSettings = httpupgradeSetting
} }
"splithttp" -> {
val splithttpSetting = SplithttpSettingsBean()
splithttpSetting.host = host.orEmpty()
sni = splithttpSetting.host
splithttpSetting.path = path ?: "/"
splithttpSettings = splithttpSetting
}
"h2", "http" -> { "h2", "http" -> {
network = "h2" network = "h2"
val h2Setting = HttpSettingsBean() val h2Setting = HttpSettingsBean()
h2Setting.host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() } h2Setting.host = (host.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() }
sni = h2Setting.host.getOrNull(0) ?: sni sni = h2Setting.host.getOrNull(0) ?: sni
h2Setting.path = path ?: "/" h2Setting.path = path ?: "/"
httpSettings = h2Setting httpSettings = h2Setting
} }
"quic" -> { "quic" -> {
val quicsetting = QuicSettingBean() val quicsetting = QuicSettingBean()
quicsetting.security = quicSecurity ?: "none" quicsetting.security = quicSecurity ?: "none"
quicsetting.key = key ?: "" quicsetting.key = key.orEmpty()
quicsetting.header.type = headerType ?: "none" quicsetting.header.type = headerType ?: "none"
quicSettings = quicsetting quicSettings = quicsetting
} }
"grpc" -> { "grpc" -> {
val grpcSetting = GrpcSettingsBean() val grpcSetting = GrpcSettingsBean()
grpcSetting.multiMode = mode == "multi" grpcSetting.multiMode = mode == "multi"
grpcSetting.serviceName = serviceName ?: "" grpcSetting.serviceName = serviceName.orEmpty()
grpcSetting.authority = authority ?: "" grpcSetting.authority = authority.orEmpty()
sni = authority ?: "" grpcSetting.idle_timeout = 60
grpcSetting.health_check_timeout = 20
sni = authority.orEmpty()
grpcSettings = grpcSetting grpcSettings = grpcSetting
} }
} }
return sni return sni
} }
fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?, fun populateTlsSettings(
publicKey: String?, shortId: String?, spiderX: String?) { streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?,
publicKey: String?, shortId: String?, spiderX: String?
) {
security = streamSecurity security = streamSecurity
val tlsSetting = TlsSettingsBean( val tlsSetting = TlsSettingsBean(
allowInsecure = allowInsecure, allowInsecure = allowInsecure,
@@ -324,18 +409,22 @@ data class V2rayConfig(
} }
} }
data class MuxBean(var enabled: Boolean, data class MuxBean(
var enabled: Boolean,
var concurrency: Int = 8, var concurrency: Int = 8,
var xudpConcurrency: Int = 8, var xudpConcurrency: Int = 8,
var xudpProxyUDP443: String = "",) var xudpProxyUDP443: String = "",
)
fun getServerAddress(): String? { fun getServerAddress(): String? {
if (protocol.equals(EConfigType.VMESS.name, true) if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true)) { || protocol.equals(EConfigType.VLESS.name, true)
) {
return settings?.vnext?.get(0)?.address return settings?.vnext?.get(0)?.address
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.SOCKS.name, true) || protocol.equals(EConfigType.SOCKS.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true)) { || protocol.equals(EConfigType.TROJAN.name, true)
) {
return settings?.servers?.get(0)?.address return settings?.servers?.get(0)?.address
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) { } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.peers?.get(0)?.endpoint?.substringBeforeLast(":") return settings?.peers?.get(0)?.endpoint?.substringBeforeLast(":")
@@ -345,11 +434,13 @@ data class V2rayConfig(
fun getServerPort(): Int? { fun getServerPort(): Int? {
if (protocol.equals(EConfigType.VMESS.name, true) if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true)) { || protocol.equals(EConfigType.VLESS.name, true)
) {
return settings?.vnext?.get(0)?.port return settings?.vnext?.get(0)?.port
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.SOCKS.name, true) || protocol.equals(EConfigType.SOCKS.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true)) { || protocol.equals(EConfigType.TROJAN.name, true)
) {
return settings?.servers?.get(0)?.port return settings?.servers?.get(0)?.port
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) { } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.peers?.get(0)?.endpoint?.substringAfterLast(":")?.toInt() return settings?.peers?.get(0)?.endpoint?.substringAfterLast(":")?.toInt()
@@ -359,10 +450,12 @@ data class V2rayConfig(
fun getPassword(): String? { fun getPassword(): String? {
if (protocol.equals(EConfigType.VMESS.name, true) if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true)) { || protocol.equals(EConfigType.VLESS.name, true)
) {
return settings?.vnext?.get(0)?.users?.get(0)?.id return settings?.vnext?.get(0)?.users?.get(0)?.id
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true)) { || protocol.equals(EConfigType.TROJAN.name, true)
) {
return settings?.servers?.get(0)?.password return settings?.servers?.get(0)?.password
} else if (protocol.equals(EConfigType.SOCKS.name, true)) { } else if (protocol.equals(EConfigType.SOCKS.name, true)) {
return settings?.servers?.get(0)?.users?.get(0)?.pass return settings?.servers?.get(0)?.users?.get(0)?.pass
@@ -385,51 +478,82 @@ data class V2rayConfig(
if (protocol.equals(EConfigType.VMESS.name, true) if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true) || protocol.equals(EConfigType.VLESS.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true) || protocol.equals(EConfigType.TROJAN.name, true)
|| protocol.equals(EConfigType.SHADOWSOCKS.name, true)) { || protocol.equals(EConfigType.SHADOWSOCKS.name, true)
) {
val transport = streamSettings?.network ?: return null val transport = streamSettings?.network ?: return null
return when (transport) { return when (transport) {
"tcp" -> { "tcp" -> {
val tcpSetting = streamSettings?.tcpSettings ?: return null val tcpSetting = streamSettings?.tcpSettings ?: return null
listOf(tcpSetting.header.type, listOf(
tcpSetting.header.type,
tcpSetting.header.request?.headers?.Host?.joinToString().orEmpty(), tcpSetting.header.request?.headers?.Host?.joinToString().orEmpty(),
tcpSetting.header.request?.path?.joinToString().orEmpty()) tcpSetting.header.request?.path?.joinToString().orEmpty()
)
} }
"kcp" -> { "kcp" -> {
val kcpSetting = streamSettings?.kcpSettings ?: return null val kcpSetting = streamSettings?.kcpSettings ?: return null
listOf(kcpSetting.header.type, listOf(
kcpSetting.header.type,
"", "",
kcpSetting.seed.orEmpty()) kcpSetting.seed.orEmpty()
)
} }
"ws" -> { "ws" -> {
val wsSetting = streamSettings?.wsSettings ?: return null val wsSetting = streamSettings?.wsSettings ?: return null
listOf("", listOf(
"",
wsSetting.headers.Host, wsSetting.headers.Host,
wsSetting.path) wsSetting.path
)
} }
"httpupgrade" -> { "httpupgrade" -> {
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
listOf("", listOf(
"",
httpupgradeSetting.host, httpupgradeSetting.host,
httpupgradeSetting.path) httpupgradeSetting.path
)
} }
"splithttp" -> {
val splithttpSetting = streamSettings?.splithttpSettings ?: return null
listOf(
"",
splithttpSetting.host,
splithttpSetting.path
)
}
"h2" -> { "h2" -> {
val h2Setting = streamSettings?.httpSettings ?: return null val h2Setting = streamSettings?.httpSettings ?: return null
listOf("", listOf(
"",
h2Setting.host.joinToString(), h2Setting.host.joinToString(),
h2Setting.path) h2Setting.path
)
} }
"quic" -> { "quic" -> {
val quicSetting = streamSettings?.quicSettings ?: return null val quicSetting = streamSettings?.quicSettings ?: return null
listOf(quicSetting.header.type, listOf(
quicSetting.header.type,
quicSetting.security, quicSetting.security,
quicSetting.key) quicSetting.key
)
} }
"grpc" -> { "grpc" -> {
val grpcSetting = streamSettings?.grpcSettings ?: return null val grpcSetting = streamSettings?.grpcSettings ?: return null
listOf(if (grpcSetting.multiMode == true) "multi" else "gun", listOf(
grpcSetting.authority ?: "", if (grpcSetting.multiMode == true) "multi" else "gun",
grpcSetting.serviceName) grpcSetting.authority.orEmpty(),
grpcSetting.serviceName
)
} }
else -> null else -> null
} }
} }
@@ -437,24 +561,29 @@ data class V2rayConfig(
} }
} }
data class DnsBean(var servers: ArrayList<Any>? = null, data class DnsBean(
var servers: ArrayList<Any>? = null,
var hosts: Map<String, Any>? = null, var hosts: Map<String, Any>? = null,
val clientIp: String? = null, val clientIp: String? = null,
val disableCache: Boolean? = null, val disableCache: Boolean? = null,
val queryStrategy: String? = null, val queryStrategy: String? = null,
val tag: String? = null val tag: String? = null
) { ) {
data class ServersBean(var address: String = "", data class ServersBean(
var address: String = "",
var port: Int? = null, var port: Int? = null,
var domains: List<String>? = null, var domains: List<String>? = null,
var expectIPs: List<String>? = null, var expectIPs: List<String>? = null,
val clientIp: String? = null) val clientIp: String? = null
)
} }
data class RoutingBean(var domainStrategy: String, data class RoutingBean(
var domainStrategy: String,
var domainMatcher: String? = null, var domainMatcher: String? = null,
var rules: ArrayList<RulesBean>, var rules: ArrayList<RulesBean>,
val balancers: List<Any>? = null) { val balancers: List<Any>? = null
) {
data class RulesBean( data class RulesBean(
var ip: ArrayList<String>? = null, var ip: ArrayList<String>? = null,
@@ -473,8 +602,10 @@ data class V2rayConfig(
) )
} }
data class PolicyBean(var levels: Map<String, LevelBean>, data class PolicyBean(
var system: Any? = null) { var levels: Map<String, LevelBean>,
var system: Any? = null
) {
data class LevelBean( data class LevelBean(
var handshake: Int? = null, var handshake: Int? = null,
var connIdle: Int? = null, var connIdle: Int? = null,
@@ -482,11 +613,14 @@ data class V2rayConfig(
var downlinkOnly: Int? = null, var downlinkOnly: Int? = null,
val statsUserUplink: Boolean? = null, val statsUserUplink: Boolean? = null,
val statsUserDownlink: Boolean? = null, val statsUserDownlink: Boolean? = null,
var bufferSize: Int? = null) var bufferSize: Int? = null
)
} }
data class FakednsBean(var ipPool: String = "198.18.0.0/15", data class FakednsBean(
var poolSize: Int = 10000) // roughly 10 times smaller than total ip pool var ipPool: String = "198.18.0.0/15",
var poolSize: Int = 10000
) // roughly 10 times smaller than total ip pool
fun getProxyOutbound(): OutboundBean? { fun getProxyOutbound(): OutboundBean? {
outbounds.forEach { outbound -> outbounds.forEach { outbound ->

View File

@@ -1,6 +1,7 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto
data class VmessQRCode(var v: String = "", data class VmessQRCode(
var v: String = "",
var ps: String = "", var ps: String = "",
var add: String = "", var add: String = "",
var port: String = "", var port: String = "",
@@ -14,4 +15,5 @@ data class VmessQRCode(var v: String = "",
var tls: String = "", var tls: String = "",
var sni: String = "", var sni: String = "",
var alpn: String = "", var alpn: String = "",
var fp: String = "") var fp: String = ""
)

View File

@@ -9,15 +9,15 @@ import org.json.JSONObject
import java.net.URI import java.net.URI
import java.net.URLConnection import java.net.URLConnection
val Context.v2RayApplication: AngApplication val Context.v2RayApplication: AngApplication?
get() = applicationContext as AngApplication get() = applicationContext as? AngApplication
fun Context.toast(message: Int) { fun Context.toast(message: Int) {
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).apply { show() } ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
} }
fun Context.toast(message: CharSequence) { fun Context.toast(message: CharSequence) {
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).apply { show() } ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
} }
fun JSONObject.putOpt(pair: Pair<String, Any?>) { fun JSONObject.putOpt(pair: Pair<String, Any?>) {
@@ -34,26 +34,14 @@ const val DIVISOR = 1024.0
fun Long.toSpeedString(): String = this.toTrafficString() + "/s" fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
fun Long.toTrafficString(): String { fun Long.toTrafficString(): String {
if (this < THRESHOLD) { val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB")
return "$this B" var size = this.toDouble()
var unitIndex = 0
while (size >= THRESHOLD && unitIndex < units.size - 1) {
size /= DIVISOR
unitIndex++
} }
val kb = this / DIVISOR return String.format("%.1f %s", size, units[unitIndex])
if (kb < THRESHOLD) {
return "${String.format("%.1f KB", kb)}"
}
val mb = kb / DIVISOR
if (mb < THRESHOLD) {
return "${String.format("%.1f MB", mb)}"
}
val gb = mb / DIVISOR
if (gb < THRESHOLD) {
return "${String.format("%.1f GB", gb)}"
}
val tb = gb / DIVISOR
if (tb < THRESHOLD) {
return "${String.format("%.1f TB", tb)}"
}
return String.format("%.1f PB", tb / DIVISOR)
} }
val URLConnection.responseLength: Long val URLConnection.responseLength: Long
@@ -64,6 +52,6 @@ val URLConnection.responseLength: Long
} }
val URI.idnHost: String val URI.idnHost: String
get() = host?.replace("[", "")?.replace("]", "") ?: "" get() = host?.replace("[", "")?.replace("]", "").orEmpty()
fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "") fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "")

View File

@@ -35,7 +35,8 @@ class WidgetProvider : AppWidgetProvider() {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else { } else {
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
}) }
)
remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent) remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent)
if (isRunning) { if (isRunning) {
remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_stop_24dp) remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_stop_24dp)
@@ -65,12 +66,17 @@ class WidgetProvider : AppWidgetProvider() {
AppWidgetManager.getInstance(context)?.let { manager -> AppWidgetManager.getInstance(context)?.let { manager ->
when (intent.getIntExtra("key", 0)) { when (intent.getIntExtra("key", 0)) {
AppConfig.MSG_STATE_RUNNING, AppConfig.MSG_STATE_START_SUCCESS -> { AppConfig.MSG_STATE_RUNNING, AppConfig.MSG_STATE_START_SUCCESS -> {
updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)), updateWidgetBackground(
true) context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
true
)
} }
AppConfig.MSG_STATE_NOT_RUNNING, AppConfig.MSG_STATE_START_FAILURE, AppConfig.MSG_STATE_STOP_SUCCESS -> { AppConfig.MSG_STATE_NOT_RUNNING, AppConfig.MSG_STATE_START_FAILURE, AppConfig.MSG_STATE_STOP_SUCCESS -> {
updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)), updateWidgetBackground(
false) context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
false
)
} }
} }
} }

View File

@@ -58,6 +58,7 @@ class QSTileService : TileService() {
Tile.STATE_INACTIVE -> { Tile.STATE_INACTIVE -> {
Utils.startVServiceFromToggle(this) Utils.startVServiceFromToggle(this)
} }
Tile.STATE_ACTIVE -> { Tile.STATE_ACTIVE -> {
Utils.stopVService(this) Utils.stopVService(this)
} }
@@ -67,22 +68,26 @@ class QSTileService : TileService() {
private var mMsgReceive: BroadcastReceiver? = null private var mMsgReceive: BroadcastReceiver? = null
private class ReceiveMessageHandler(context: QSTileService) : BroadcastReceiver() { private class ReceiveMessageHandler(context: QSTileService) : BroadcastReceiver() {
internal var mReference: SoftReference<QSTileService> = SoftReference(context) var mReference: SoftReference<QSTileService> = SoftReference(context)
override fun onReceive(ctx: Context?, intent: Intent?) { override fun onReceive(ctx: Context?, intent: Intent?) {
val context = mReference.get() val context = mReference.get()
when (intent?.getIntExtra("key", 0)) { when (intent?.getIntExtra("key", 0)) {
AppConfig.MSG_STATE_RUNNING -> { AppConfig.MSG_STATE_RUNNING -> {
context?.setState(Tile.STATE_ACTIVE) context?.setState(Tile.STATE_ACTIVE)
} }
AppConfig.MSG_STATE_NOT_RUNNING -> { AppConfig.MSG_STATE_NOT_RUNNING -> {
context?.setState(Tile.STATE_INACTIVE) context?.setState(Tile.STATE_INACTIVE)
} }
AppConfig.MSG_STATE_START_SUCCESS -> { AppConfig.MSG_STATE_START_SUCCESS -> {
context?.setState(Tile.STATE_ACTIVE) context?.setState(Tile.STATE_ACTIVE)
} }
AppConfig.MSG_STATE_START_FAILURE -> { AppConfig.MSG_STATE_START_FAILURE -> {
context?.setState(Tile.STATE_INACTIVE) context?.setState(Tile.STATE_INACTIVE)
} }
AppConfig.MSG_STATE_STOP_SUCCESS -> { AppConfig.MSG_STATE_STOP_SUCCESS -> {
context?.setState(Tile.STATE_INACTIVE) context?.setState(Tile.STATE_INACTIVE)
} }

View File

@@ -11,6 +11,8 @@ import androidx.core.app.NotificationManagerCompat
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.util.AngConfigManager import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager
@@ -18,14 +20,13 @@ import com.v2ray.ang.util.Utils
object SubscriptionUpdater { object SubscriptionUpdater {
const val notificationChannel = "subscription_update_channel"
class UpdateTask(context: Context, params: WorkerParameters) : class UpdateTask(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) { CoroutineWorker(context, params) {
private val notificationManager = NotificationManagerCompat.from(applicationContext) private val notificationManager = NotificationManagerCompat.from(applicationContext)
private val notification = private val notification =
NotificationCompat.Builder(applicationContext, notificationChannel) NotificationCompat.Builder(applicationContext, SUBSCRIPTION_UPDATE_CHANNEL)
.setWhen(0) .setWhen(0)
.setTicker("Update") .setTicker("Update")
.setContentTitle(context.getString(R.string.title_pref_auto_update_subscription)) .setContentTitle(context.getString(R.string.title_pref_auto_update_subscription))
@@ -43,11 +44,11 @@ object SubscriptionUpdater {
val subscription = i.second val subscription = i.second
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notification.setChannelId(notificationChannel) notification.setChannelId(SUBSCRIPTION_UPDATE_CHANNEL)
val channel = val channel =
NotificationChannel( NotificationChannel(
notificationChannel, SUBSCRIPTION_UPDATE_CHANNEL,
"Subscription Update Service", SUBSCRIPTION_UPDATE_CHANNEL_NAME,
NotificationManager.IMPORTANCE_MIN NotificationManager.IMPORTANCE_MIN
) )
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
@@ -58,23 +59,11 @@ object SubscriptionUpdater {
"subscription automatic update: ---${subscription.remarks}" "subscription automatic update: ---${subscription.remarks}"
) )
val configs = Utils.getUrlContentWithCustomUserAgent(subscription.url) val configs = Utils.getUrlContentWithCustomUserAgent(subscription.url)
importBatchConfig(configs, i.first) AngConfigManager.importBatchConfig(configs, i.first, false)
notification.setContentText("Updating ${subscription.remarks}") notification.setContentText("Updating ${subscription.remarks}")
} }
notificationManager.cancel(3) notificationManager.cancel(3)
return Result.success() return Result.success()
} }
} }
fun importBatchConfig(server: String?, subid: String = "") {
val append = subid.isEmpty()
val count = AngConfigManager.importBatchConfig(server, subid, append)
if (count <= 0) {
AngConfigManager.importBatchConfig(Utils.decode(server!!), subid, append)
}
if (count <= 0) {
AngConfigManager.appendCustomConfigServer(server, subid)
}
}
} }

View File

@@ -49,7 +49,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
val context = newBase?.let { val context = newBase?.let {
MyContextWrapper.wrap(newBase, Utils.getLocale(newBase)) MyContextWrapper.wrap(newBase, Utils.getLocale())
} }
super.attachBaseContext(context) super.attachBaseContext(context)
} }

View File

@@ -27,14 +27,14 @@ import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.V2rayConfigUtil import com.v2ray.ang.util.V2rayConfigUtil
import go.Seq import go.Seq
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import libv2ray.Libv2ray import libv2ray.Libv2ray
import libv2ray.V2RayPoint import libv2ray.V2RayPoint
import libv2ray.V2RayVPNServiceSupportsSet import libv2ray.V2RayVPNServiceSupportsSet
import rx.Observable
import rx.Subscription
import java.lang.ref.SoftReference import java.lang.ref.SoftReference
import kotlin.math.min import kotlin.math.min
@@ -59,16 +59,21 @@ object V2RayServiceManager {
private var lastQueryTime = 0L private var lastQueryTime = 0L
private var mBuilder: NotificationCompat.Builder? = null private var mBuilder: NotificationCompat.Builder? = null
private var mSubscription: Subscription? = null private var mDisposable: Disposable? = null
private var mNotificationManager: NotificationManager? = null private var mNotificationManager: NotificationManager? = null
fun startV2Ray(context: Context) { fun startV2Ray(context: Context) {
if (v2rayPoint.isRunning) return
val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
val result = V2rayConfigUtil.getV2rayConfig(context, guid)
if (!result.status) return
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) { if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) {
context.toast(R.string.toast_warning_pref_proxysharing_short) context.toast(R.string.toast_warning_pref_proxysharing_short)
} else { } else {
context.toast(R.string.toast_services_start) context.toast(R.string.toast_services_start)
} }
val intent = if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") { val intent = if ((settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN") == "VPN") {
Intent(context.applicationContext, V2RayVpnService::class.java) Intent(context.applicationContext, V2RayVpnService::class.java)
} else { } else {
Intent(context.applicationContext, V2RayProxyOnlyService::class.java) Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
@@ -126,7 +131,9 @@ object V2RayServiceManager {
val service = serviceControl?.get()?.getService() ?: return val service = serviceControl?.get()?.getService() ?: return
val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
val config = MmkvManager.decodeServerConfig(guid) ?: return val config = MmkvManager.decodeServerConfig(guid) ?: return
if (!v2rayPoint.isRunning) { if (v2rayPoint.isRunning) {
return
}
val result = V2rayConfigUtil.getV2rayConfig(service, guid) val result = V2rayConfigUtil.getV2rayConfig(service, guid)
if (!result.status) if (!result.status)
return return
@@ -163,13 +170,12 @@ object V2RayServiceManager {
cancelNotification() cancelNotification()
} }
} }
}
fun stopV2rayPoint() { fun stopV2rayPoint() {
val service = serviceControl?.get()?.getService() ?: return val service = serviceControl?.get()?.getService() ?: return
if (v2rayPoint.isRunning) { if (v2rayPoint.isRunning) {
GlobalScope.launch(Dispatchers.Default) { CoroutineScope(Dispatchers.IO).launch {
try { try {
v2rayPoint.stopLoop() v2rayPoint.stopLoop()
} catch (e: Exception) { } catch (e: Exception) {
@@ -200,18 +206,23 @@ object V2RayServiceManager {
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "") MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
} }
} }
AppConfig.MSG_UNREGISTER_CLIENT -> { AppConfig.MSG_UNREGISTER_CLIENT -> {
// nothing to do // nothing to do
} }
AppConfig.MSG_STATE_START -> { AppConfig.MSG_STATE_START -> {
// nothing to do // nothing to do
} }
AppConfig.MSG_STATE_STOP -> { AppConfig.MSG_STATE_STOP -> {
serviceControl.stopService() serviceControl.stopService()
} }
AppConfig.MSG_STATE_RESTART -> { AppConfig.MSG_STATE_RESTART -> {
startV2rayPoint() startV2rayPoint()
} }
AppConfig.MSG_MEASURE_DELAY -> { AppConfig.MSG_MEASURE_DELAY -> {
measureV2rayDelay() measureV2rayDelay()
} }
@@ -222,6 +233,7 @@ object V2RayServiceManager {
Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats") Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
stopSpeedNotification() stopSpeedNotification()
} }
Intent.ACTION_SCREEN_ON -> { Intent.ACTION_SCREEN_ON -> {
Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats") Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats")
startSpeedNotification() startSpeedNotification()
@@ -231,17 +243,25 @@ object V2RayServiceManager {
} }
private fun measureV2rayDelay() { private fun measureV2rayDelay() {
GlobalScope.launch(Dispatchers.IO) { CoroutineScope(Dispatchers.IO).launch {
val service = serviceControl?.get()?.getService() ?: return@launch val service = serviceControl?.get()?.getService() ?: return@launch
var time = -1L var time = -1L
var errstr = "" var errstr = ""
if (v2rayPoint.isRunning) { if (v2rayPoint.isRunning) {
try { try {
time = v2rayPoint.measureDelay() time = v2rayPoint.measureDelay(Utils.getDelayTestUrl())
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e") Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
errstr = e.message?.substringAfter("\":") ?: "empty message" errstr = e.message?.substringAfter("\":") ?: "empty message"
} }
if (time == -1L) {
try {
time = v2rayPoint.measureDelay(Utils.getDelayTestUrl(true))
} catch (e: Exception) {
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
errstr = e.message?.substringAfter("\":") ?: "empty message"
}
}
} }
val result = if (time == -1L) { val result = if (time == -1L) {
service.getString(R.string.connection_test_error, errstr) service.getString(R.string.connection_test_error, errstr)
@@ -256,25 +276,29 @@ object V2RayServiceManager {
private fun showNotification() { private fun showNotification() {
val service = serviceControl?.get()?.getService() ?: return val service = serviceControl?.get()?.getService() ?: return
val startMainIntent = Intent(service, MainActivity::class.java) val startMainIntent = Intent(service, MainActivity::class.java)
val contentPendingIntent = PendingIntent.getActivity(service, val contentPendingIntent = PendingIntent.getActivity(
service,
NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else { } else {
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
}) }
)
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE) val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
stopV2RayIntent.`package` = ANG_PACKAGE stopV2RayIntent.`package` = ANG_PACKAGE
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP) stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
val stopV2RayPendingIntent = PendingIntent.getBroadcast(service, val stopV2RayPendingIntent = PendingIntent.getBroadcast(
service,
NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else { } else {
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
}) }
)
val channelId = val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -293,9 +317,11 @@ object V2RayServiceManager {
.setShowWhen(false) .setShowWhen(false)
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
.setContentIntent(contentPendingIntent) .setContentIntent(contentPendingIntent)
.addAction(R.drawable.ic_delete_24dp, .addAction(
R.drawable.ic_delete_24dp,
service.getString(R.string.notification_action_stop_v2ray), service.getString(R.string.notification_action_stop_v2ray),
stopV2RayPendingIntent) stopV2RayPendingIntent
)
//.build() //.build()
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使 //mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
@@ -305,10 +331,12 @@ object V2RayServiceManager {
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(): String { private fun createNotificationChannel(): String {
val channelId = "RAY_NG_M_CH_ID" val channelId = AppConfig.RAY_NG_CHANNEL_ID
val channelName = "V2rayNG Background Service" val channelName = AppConfig.RAY_NG_CHANNEL_NAME
val chan = NotificationChannel(channelId, val chan = NotificationChannel(
channelName, NotificationManager.IMPORTANCE_HIGH) channelId,
channelName, NotificationManager.IMPORTANCE_HIGH
)
chan.lightColor = Color.DKGRAY chan.lightColor = Color.DKGRAY
chan.importance = NotificationManager.IMPORTANCE_NONE chan.importance = NotificationManager.IMPORTANCE_NONE
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
@@ -320,8 +348,8 @@ object V2RayServiceManager {
val service = serviceControl?.get()?.getService() ?: return val service = serviceControl?.get()?.getService() ?: return
service.stopForeground(true) service.stopForeground(true)
mBuilder = null mBuilder = null
mSubscription?.unsubscribe() mDisposable?.dispose()
mSubscription = null mDisposable = null
} }
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) { private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
@@ -348,36 +376,39 @@ object V2RayServiceManager {
} }
private fun startSpeedNotification() { private fun startSpeedNotification() {
if (mSubscription == null && if (mDisposable == null &&
v2rayPoint.isRunning && v2rayPoint.isRunning &&
settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true) { settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true
) {
var lastZeroSpeed = false var lastZeroSpeed = false
val outboundTags = currentConfig?.getAllOutboundTags() val outboundTags = currentConfig?.getAllOutboundTags()
outboundTags?.remove(TAG_DIRECT) outboundTags?.remove(TAG_DIRECT)
mSubscription = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS) mDisposable = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
.subscribe { .subscribe {
val queryTime = System.currentTimeMillis() val queryTime = System.currentTimeMillis()
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0 val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
var proxyTotal = 0L var proxyTotal = 0L
val text = StringBuilder() val text = StringBuilder()
outboundTags?.forEach { outboundTags?.forEach {
val up = v2rayPoint.queryStats(it, "uplink") val up = v2rayPoint.queryStats(it, AppConfig.UPLINK)
val down = v2rayPoint.queryStats(it, "downlink") val down = v2rayPoint.queryStats(it, AppConfig.DOWNLINK)
if (up + down > 0) { if (up + down > 0) {
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds) appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
proxyTotal += up + down proxyTotal += up + down
} }
} }
val directUplink = v2rayPoint.queryStats(TAG_DIRECT, "uplink") val directUplink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.UPLINK)
val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, "downlink") val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.DOWNLINK)
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
if (!zeroSpeed || !lastZeroSpeed) { if (!zeroSpeed || !lastZeroSpeed) {
if (proxyTotal == 0L) { if (proxyTotal == 0L) {
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0) appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
} }
appendSpeedString(text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds, appendSpeedString(
directDownlink / sinceLastQueryInSeconds) text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
directDownlink / sinceLastQueryInSeconds
)
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink) updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
} }
lastZeroSpeed = zeroSpeed lastZeroSpeed = zeroSpeed
@@ -397,9 +428,9 @@ object V2RayServiceManager {
} }
private fun stopSpeedNotification() { private fun stopSpeedNotification() {
if (mSubscription != null) { if (mDisposable != null) {
mSubscription?.unsubscribe() //stop queryStats mDisposable?.dispose() //stop queryStats
mSubscription = null mDisposable = null
updateNotification(currentConfig?.remarks, 0, 0) updateNotification(currentConfig?.remarks, 0, 0)
} }
} }

View File

@@ -10,7 +10,11 @@ import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.SpeedtestUtil import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import go.Seq import go.Seq
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import libv2ray.Libv2ray import libv2ray.Libv2ray
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -32,6 +36,7 @@ class V2RayTestService : Service() {
MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(contentPair.first, result)) MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(contentPair.first, result))
} }
} }
MSG_MEASURE_CONFIG_CANCEL -> { MSG_MEASURE_CONFIG_CANCEL -> {
realTestScope.coroutineContext[Job]?.cancelChildren() realTestScope.coroutineContext[Job]?.cancelChildren()
} }

View File

@@ -4,7 +4,13 @@ import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.* import android.net.ConnectivityManager
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.VpnService
import android.os.Build import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.os.StrictMode import android.os.StrictMode
@@ -17,8 +23,8 @@ import com.v2ray.ang.dto.ERoutingMode
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MyContextWrapper import com.v2ray.ang.util.MyContextWrapper
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.lang.ref.SoftReference import java.lang.ref.SoftReference
@@ -196,14 +202,16 @@ class V2RayVpnService : VpnService(), ServiceControl {
private fun runTun2socks() { private fun runTun2socks() {
val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt()) val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
val cmd = arrayListOf(File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath, val cmd = arrayListOf(
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER, "--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
"--netif-netmask", "255.255.255.252", "--netif-netmask", "255.255.255.252",
"--socks-server-addr", "127.0.0.1:${socksPort}", "--socks-server-addr", "127.0.0.1:${socksPort}",
"--tunmtu", VPN_MTU.toString(), "--tunmtu", VPN_MTU.toString(),
"--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath, "--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
"--enable-udprelay", "--enable-udprelay",
"--loglevel", "notice") "--loglevel", "notice"
)
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) { if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
cmd.add("--netif-ip6addr") cmd.add("--netif-ip6addr")
@@ -223,11 +231,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
.directory(applicationContext.filesDir) .directory(applicationContext.filesDir)
.start() .start()
Thread(Runnable { Thread(Runnable {
Log.d(packageName,"$TUN2SOCKS check") Log.d(packageName, "$TUN2SOCKS check")
process.waitFor() process.waitFor()
Log.d(packageName,"$TUN2SOCKS exited") Log.d(packageName, "$TUN2SOCKS exited")
if (isRunning) { if (isRunning) {
Log.d(packageName,"$TUN2SOCKS restart") Log.d(packageName, "$TUN2SOCKS restart")
runTun2socks() runTun2socks()
} }
}).start() }).start()
@@ -244,7 +252,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
val path = File(applicationContext.filesDir, "sock_path").absolutePath val path = File(applicationContext.filesDir, "sock_path").absolutePath
Log.d(packageName, path) Log.d(packageName, path)
GlobalScope.launch(Dispatchers.IO) { CoroutineScope(Dispatchers.IO).launch {
var tries = 0 var tries = 0
while (true) try { while (true) try {
Thread.sleep(50L shl tries) Thread.sleep(50L shl tries)
@@ -274,7 +282,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
// val emptyInfo = VpnNetworkInfo() // val emptyInfo = VpnNetworkInfo()
// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo) // val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
// saveVpnNetworkInfo(configName, info) // saveVpnNetworkInfo(configName, info)
isRunning = false; isRunning = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try { try {
connectivity.unregisterNetworkCallback(defaultNetworkCallback) connectivity.unregisterNetworkCallback(defaultNetworkCallback)
@@ -327,7 +335,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
val context = newBase?.let { val context = newBase?.let {
MyContextWrapper.wrap(newBase, Utils.getLocale(newBase)) MyContextWrapper.wrap(newBase, Utils.getLocale())
} }
super.attachBaseContext(context) super.attachBaseContext(context)
} }

View File

@@ -5,7 +5,8 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import com.tbruyelle.rxpermissions.RxPermissions import androidx.core.content.FileProvider
import com.tbruyelle.rxpermissions3.RxPermissions
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.BuildConfig import com.v2ray.ang.BuildConfig
@@ -20,20 +21,42 @@ import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class AboutActivity : BaseActivity() { class AboutActivity : BaseActivity() {
private lateinit var binding: ActivityAboutBinding private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) }
private val extDir by lazy { File(Utils.backupPath(this)) } private val extDir by lazy { File(Utils.backupPath(this)) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityAboutBinding.inflate(layoutInflater) setContentView(binding.root)
val view = binding.root
setContentView(view)
title = getString(R.string.title_about) title = getString(R.string.title_about)
binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir) binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir)
binding.layoutBackup.setOnClickListener { binding.layoutBackup.setOnClickListener {
backupMMKV() val ret = backupConfiguration(extDir.absolutePath)
if (ret.first) {
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
}
binding.layoutShare.setOnClickListener {
val ret = backupConfiguration(cacheDir.absolutePath)
if (ret.first) {
startActivity(
Intent.createChooser(
Intent(Intent.ACTION_SEND).setType("application/zip")
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(
Intent.EXTRA_STREAM, FileProvider.getUriForFile(
this, BuildConfig.APPLICATION_ID + ".cache", File(ret.second)
)
), getString(R.string.title_configuration_share)
)
)
} else {
toast(R.string.toast_failure)
}
} }
binding.layoutRestore.setOnClickListener { binding.layoutRestore.setOnClickListener {
@@ -77,40 +100,36 @@ class AboutActivity : BaseActivity() {
} }
} }
fun backupMMKV() { fun backupConfiguration(outputZipFilePos: String): Pair<Boolean, String> {
val dateFormated = SimpleDateFormat( val dateFormated = SimpleDateFormat(
"yyyy-MM-dd-HH-mm-ss", "yyyy-MM-dd-HH-mm-ss",
Locale.getDefault() Locale.getDefault()
).format(System.currentTimeMillis()) ).format(System.currentTimeMillis())
val folderName = "${getString(R.string.app_name)}_${dateFormated}" val folderName = "${getString(R.string.app_name)}_${dateFormated}"
val backupDir = this.cacheDir.absolutePath + "/$folderName" val backupDir = this.cacheDir.absolutePath + "/$folderName"
val outputZipFilePath = extDir.absolutePath + "/$folderName.zip" val outputZipFilePath = "$outputZipFilePos/$folderName.zip"
val count = MMKV.backupAllToDirectory(backupDir) val count = MMKV.backupAllToDirectory(backupDir)
if (count <= 0) { if (count <= 0) {
toast(R.string.toast_failure) return Pair(false, "")
} }
if (ZipUtil.zipFromFolder(backupDir, outputZipFilePath)) { if (ZipUtil.zipFromFolder(backupDir, outputZipFilePath)) {
toast(R.string.toast_success) return Pair(true, outputZipFilePath)
} else { } else {
toast(R.string.toast_failure) return Pair(false, "")
} }
} }
fun restoreMMKV(zipFile: File) { fun restoreConfiguration(zipFile: File): Boolean {
val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}" val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}"
if (!ZipUtil.unzipToFolder(zipFile, backupDir)) { if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
toast(R.string.toast_failure) return false
} }
val count = MMKV.restoreAllFromDirectory(backupDir) val count = MMKV.restoreAllFromDirectory(backupDir)
if (count > 0) { return count > 0
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
} }
private fun showFileChooser() { private fun showFileChooser() {
@@ -138,8 +157,11 @@ class AboutActivity : BaseActivity() {
input?.copyTo(fileOut) input?.copyTo(fileOut)
} }
} }
if (restoreConfiguration(targetFile)) {
restoreMMKV(targetFile) toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }

View File

@@ -23,16 +23,19 @@ abstract class BaseActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
onBackPressed() // Handles the home button press by delegating to the onBackPressedDispatcher.
// This ensures consistent back navigation behavior.
onBackPressedDispatcher.onBackPressed()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
val context = newBase?.let { val context = newBase?.let {
MyContextWrapper.wrap(newBase, Utils.getLocale(newBase)) MyContextWrapper.wrap(newBase, Utils.getLocale())
} }
super.attachBaseContext(context) super.attachBaseContext(context)
} }

View File

@@ -1,8 +1,8 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Bundle
import android.text.method.ScrollingMovementMethod import android.text.method.ScrollingMovementMethod
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@@ -14,20 +14,18 @@ import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException import java.io.IOException
import java.util.LinkedHashSet
class LogcatActivity : BaseActivity() { class LogcatActivity : BaseActivity() {
private lateinit var binding: ActivityLogcatBinding private val binding by lazy {
ActivityLogcatBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityLogcatBinding.inflate(layoutInflater) setContentView(binding.root)
val view = binding.root
setContentView(view)
title = getString(R.string.title_logcat) title = getString(R.string.title_logcat)
@@ -44,9 +42,11 @@ class LogcatActivity : BaseActivity() {
val lst = LinkedHashSet<String>() val lst = LinkedHashSet<String>()
lst.add("logcat") lst.add("logcat")
lst.add("-c") lst.add("-c")
withContext(Dispatchers.IO) {
val process = Runtime.getRuntime().exec(lst.toTypedArray()) val process = Runtime.getRuntime().exec(lst.toTypedArray())
process.waitFor() process.waitFor()
} }
}
val lst = LinkedHashSet<String>() val lst = LinkedHashSet<String>()
lst.add("logcat") lst.add("logcat")
lst.add("-d") lst.add("-d")
@@ -54,7 +54,9 @@ class LogcatActivity : BaseActivity() {
lst.add("time") lst.add("time")
lst.add("-s") lst.add("-s")
lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err") lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
val process = Runtime.getRuntime().exec(lst.toTypedArray()) val process = withContext(Dispatchers.IO) {
Runtime.getRuntime().exec(lst.toTypedArray())
}
// val bufferedReader = BufferedReader( // val bufferedReader = BufferedReader(
// InputStreamReader(process.inputStream)) // InputStreamReader(process.inputStream))
// val allText = bufferedReader.use(BufferedReader::readText) // val allText = bufferedReader.use(BufferedReader::readText)
@@ -82,10 +84,12 @@ class LogcatActivity : BaseActivity() {
toast(R.string.toast_success) toast(R.string.toast_success)
true true
} }
R.id.clear_all -> { R.id.clear_all -> {
logcat(true) logcat(true)
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }

View File

@@ -9,7 +9,6 @@ import android.net.VpnService
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@@ -19,16 +18,17 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.tbruyelle.rxpermissions.RxPermissions import com.google.android.material.tabs.TabLayout
import com.tencent.mmkv.MMKV import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityMainBinding import com.v2ray.ang.databinding.ActivityMainBinding
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
@@ -39,41 +39,55 @@ import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.MainViewModel import com.v2ray.ang.viewmodel.MainViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.drakeet.support.toast.ToastCompat import me.drakeet.support.toast.ToastCompat
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener { class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
private lateinit var binding: ActivityMainBinding private val binding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
private val adapter by lazy { MainRecyclerAdapter(this) } private val adapter by lazy { MainRecyclerAdapter(this) }
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) { if (it.resultCode == RESULT_OK) {
startV2Ray() startV2Ray()
} }
} }
private val requestSubSettingActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
initGroupTab()
}
private val tabGroupListener = object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
val selectId = tab?.tag.toString()
if (selectId != mainViewModel.subscriptionId) {
mainViewModel.subscriptionIdChanged(selectId)
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
}
private var mItemTouchHelper: ItemTouchHelper? = null private var mItemTouchHelper: ItemTouchHelper? = null
val mainViewModel: MainViewModel by viewModels() val mainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root)
val view = binding.root
setContentView(view)
title = getString(R.string.title_server) title = getString(R.string.title_server)
setSupportActionBar(binding.toolbar) setSupportActionBar(binding.toolbar)
binding.fab.setOnClickListener { binding.fab.setOnClickListener {
if (mainViewModel.isRunning.value == true) { if (mainViewModel.isRunning.value == true) {
Utils.stopVService(this) Utils.stopVService(this)
} else if ((settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN") == "VPN") { } else if ((MmkvManager.settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN") == "VPN") {
val intent = VpnService.prepare(this) val intent = VpnService.prepare(this)
if (intent == null) { if (intent == null) {
startV2Ray() startV2Ray()
@@ -103,15 +117,14 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
val toggle = ActionBarDrawerToggle( val toggle = ActionBarDrawerToggle(
this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
)
binding.drawerLayout.addDrawerListener(toggle) binding.drawerLayout.addDrawerListener(toggle)
toggle.syncState() toggle.syncState()
binding.navView.setNavigationItemSelectedListener(this) binding.navView.setNavigationItemSelectedListener(this)
initGroupTab()
setupViewModel() setupViewModel()
copyAssets()
//migrateLegacy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
RxPermissions(this) RxPermissions(this)
@@ -158,49 +171,34 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
} }
mainViewModel.startListenBroadcast() mainViewModel.startListenBroadcast()
mainViewModel.copyAssets(assets)
} }
private fun copyAssets() { private fun initGroupTab() {
val extFolder = Utils.userAssetPath(this) binding.tabGroup.removeOnTabSelectedListener(tabGroupListener)
lifecycleScope.launch(Dispatchers.IO) { binding.tabGroup.removeAllTabs()
try { binding.tabGroup.isVisible = false
val geo = arrayOf("geosite.dat", "geoip.dat")
assets.list("") val (listId, listRemarks) = mainViewModel.getSubscriptions(this)
?.filter { geo.contains(it) } if (listId == null || listRemarks == null) {
?.filter { !File(extFolder, it).exists() } return
?.forEach {
val target = File(extFolder, it)
assets.open(it).use { input ->
FileOutputStream(target).use { output ->
input.copyTo(output)
}
}
Log.i(ANG_PACKAGE, "Copied from apk assets folder to ${target.absolutePath}")
}
} catch (e: Exception) {
Log.e(ANG_PACKAGE, "asset copy failed", e)
}
}
} }
// private fun migrateLegacy() { for (it in listRemarks.indices) {
// lifecycleScope.launch(Dispatchers.IO) { val tab = binding.tabGroup.newTab()
// val result = AngConfigManager.migrateLegacyConfig(this@MainActivity) tab.text = listRemarks[it]
// if (result != null) { tab.tag = listId[it]
// launch(Dispatchers.Main) { binding.tabGroup.addTab(tab)
// if (result) { }
// toast(getString(R.string.migration_success)) val selectIndex =
// mainViewModel.reloadServerList() listId.indexOf(mainViewModel.subscriptionId).takeIf { it >= 0 } ?: (listId.count() - 1)
// } else { binding.tabGroup.selectTab(binding.tabGroup.getTabAt(selectIndex))
// toast(getString(R.string.migration_fail)) binding.tabGroup.addOnTabSelectedListener(tabGroupListener)
// } binding.tabGroup.isVisible = true
// } }
// }
// }
// }
fun startV2Ray() { fun startV2Ray() {
if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) { if (MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
return return
} }
V2RayServiceManager.startV2Ray(this) V2RayServiceManager.startV2Ray(this)
@@ -228,7 +226,25 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu) menuInflater.inflate(R.menu.menu_main, menu)
return true
val searchItem = menu.findItem(R.id.search_view)
if (searchItem != null) {
val searchView = searchItem.actionView as SearchView
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
mainViewModel.filterConfig(newText.orEmpty())
return false
}
})
searchView.setOnCloseListener {
mainViewModel.filterConfig("")
false
}
}
return super.onCreateOptionsMenu(menu)
} }
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
@@ -236,67 +252,80 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
importQRcode(true) importQRcode(true)
true true
} }
R.id.import_clipboard -> { R.id.import_clipboard -> {
importClipboard() importClipboard()
true true
} }
R.id.import_manually_vmess -> { R.id.import_manually_vmess -> {
importManually(EConfigType.VMESS.value) importManually(EConfigType.VMESS.value)
true true
} }
R.id.import_manually_vless -> { R.id.import_manually_vless -> {
importManually(EConfigType.VLESS.value) importManually(EConfigType.VLESS.value)
true true
} }
R.id.import_manually_ss -> { R.id.import_manually_ss -> {
importManually(EConfigType.SHADOWSOCKS.value) importManually(EConfigType.SHADOWSOCKS.value)
true true
} }
R.id.import_manually_socks -> { R.id.import_manually_socks -> {
importManually(EConfigType.SOCKS.value) importManually(EConfigType.SOCKS.value)
true true
} }
R.id.import_manually_trojan -> { R.id.import_manually_trojan -> {
importManually(EConfigType.TROJAN.value) importManually(EConfigType.TROJAN.value)
true true
} }
R.id.import_manually_wireguard -> { R.id.import_manually_wireguard -> {
importManually(EConfigType.WIREGUARD.value) importManually(EConfigType.WIREGUARD.value)
true true
} }
R.id.import_config_custom_clipboard -> { R.id.import_config_custom_clipboard -> {
importConfigCustomClipboard() importConfigCustomClipboard()
true true
} }
R.id.import_config_custom_local -> { R.id.import_config_custom_local -> {
importConfigCustomLocal() importConfigCustomLocal()
true true
} }
R.id.import_config_custom_url -> { R.id.import_config_custom_url -> {
importConfigCustomUrlClipboard() importConfigCustomUrlClipboard()
true true
} }
R.id.import_config_custom_url_scan -> { R.id.import_config_custom_url_scan -> {
importQRcode(false) importQRcode(false)
true true
} }
// R.id.sub_setting -> {
// startActivity<SubSettingActivity>()
// true
// }
R.id.sub_update -> { R.id.sub_update -> {
importConfigViaSub() importConfigViaSub()
true true
} }
R.id.export_all -> { R.id.export_all -> {
if (AngConfigManager.shareNonCustomConfigsToClipboard(this, mainViewModel.serverList) == 0) { binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
val ret = mainViewModel.exportAllServer()
launch(Dispatchers.Main) {
if (ret == 0)
toast(R.string.toast_success) toast(R.string.toast_success)
} else { else
toast(R.string.toast_failure) toast(R.string.toast_failure)
binding.pbWaiting.hide()
} }
}
true true
} }
@@ -318,52 +347,77 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
R.id.del_all_config -> { R.id.del_all_config -> {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
MmkvManager.removeAllServer() binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
mainViewModel.removeAllServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
binding.pbWaiting.hide()
} }
.setNegativeButton(android.R.string.no) {_, _ -> }
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting //do noting
} }
.show() .show()
true true
} }
R.id.del_duplicate_config-> {
R.id.del_duplicate_config -> {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
mainViewModel.removeDuplicateServer() binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
val ret = mainViewModel.removeDuplicateServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
toast(getString(R.string.title_del_duplicate_config_count, ret))
binding.pbWaiting.hide()
} }
.setNegativeButton(android.R.string.no) {_, _ -> }
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting //do noting
} }
.show() .show()
true true
} }
R.id.del_invalid_config -> { R.id.del_invalid_config -> {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
MmkvManager.removeInvalidServer() binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
mainViewModel.removeInvalidServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
binding.pbWaiting.hide()
} }
.setNegativeButton(android.R.string.no) {_, _ -> }
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting //do noting
} }
.show() .show()
true true
} }
R.id.sort_by_test_results -> { R.id.sort_by_test_results -> {
MmkvManager.sortByTestResults() binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
mainViewModel.sortByTestResults()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
true binding.pbWaiting.hide()
}
} }
R.id.filter_config -> {
mainViewModel.filterConfig(this)
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
private fun importManually(createConfigType : Int) { private fun importManually(createConfigType: Int) {
startActivity( startActivity(
Intent() Intent()
.putExtra("createConfigType", createConfigType) .putExtra("createConfigType", createConfigType)
@@ -375,7 +429,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/** /**
* import config from qrcode * import config from qrcode
*/ */
fun importQRcode(forConfig: Boolean): Boolean { private fun importQRcode(forConfig: Boolean): Boolean {
// try { // try {
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN") // startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
// .addCategory(Intent.CATEGORY_DEFAULT) // .addCategory(Intent.CATEGORY_DEFAULT)
@@ -411,7 +465,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/** /**
* import config from clipboard * import config from clipboard
*/ */
fun importClipboard() private fun importClipboard()
: Boolean { : Boolean {
try { try {
val clipboard = Utils.getClipboard(this) val clipboard = Utils.getClipboard(this)
@@ -423,30 +477,32 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
return true return true
} }
fun importBatchConfig(server: String?, subid: String = "") { private fun importBatchConfig(server: String?) {
val subid2 = if(subid.isNullOrEmpty()){ // val dialog = AlertDialog.Builder(this)
mainViewModel.subscriptionId // .setView(LayoutProgressBinding.inflate(layoutInflater).root)
}else{ // .setCancelable(false)
subid // .show()
} binding.pbWaiting.show()
val append = subid.isNullOrEmpty()
var count = AngConfigManager.importBatchConfig(server, subid2, append) lifecycleScope.launch(Dispatchers.IO) {
if (count <= 0) { val (count, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true)
count = AngConfigManager.importBatchConfig(Utils.decode(server!!), subid2, append) delay(500L)
} launch(Dispatchers.Main) {
if (count <= 0) {
count = AngConfigManager.appendCustomConfigServer(server, subid2)
}
if (count > 0) { if (count > 0) {
toast(R.string.toast_success) toast(R.string.toast_success)
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
} else if (countSub > 0) {
initGroupTab()
} else { } else {
toast(R.string.toast_failure) toast(R.string.toast_failure)
} }
//dialog.dismiss()
binding.pbWaiting.hide()
}
}
} }
fun importConfigCustomClipboard() private fun importConfigCustomClipboard()
: Boolean { : Boolean {
try { try {
val configText = Utils.getClipboard(this) val configText = Utils.getClipboard(this)
@@ -465,7 +521,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/** /**
* import config from local config file * import config from local config file
*/ */
fun importConfigCustomLocal(): Boolean { private fun importConfigCustomLocal(): Boolean {
try { try {
showFileChooser() showFileChooser()
} catch (e: Exception) { } catch (e: Exception) {
@@ -475,7 +531,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
return true return true
} }
fun importConfigCustomUrlClipboard() private fun importConfigCustomUrlClipboard()
: Boolean { : Boolean {
try { try {
val url = Utils.getClipboard(this) val url = Utils.getClipboard(this)
@@ -493,7 +549,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/** /**
* import config from url * import config from url
*/ */
fun importConfigCustomUrl(url: String?): Boolean { private fun importConfigCustomUrl(url: String?): Boolean {
try { try {
if (!Utils.isValidUrl(url)) { if (!Utils.isValidUrl(url)) {
toast(R.string.toast_invalid_url) toast(R.string.toast_invalid_url)
@@ -520,43 +576,26 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/** /**
* import config from sub * import config from sub
*/ */
fun importConfigViaSub() private fun importConfigViaSub(): Boolean {
: Boolean { // val dialog = AlertDialog.Builder(this)
try { // .setView(LayoutProgressBinding.inflate(layoutInflater).root)
toast(R.string.title_sub_update) // .setCancelable(false)
MmkvManager.decodeSubscriptions().forEach { // .show()
if (TextUtils.isEmpty(it.first) binding.pbWaiting.show()
|| TextUtils.isEmpty(it.second.remarks)
|| TextUtils.isEmpty(it.second.url)
) {
return@forEach
}
if (!it.second.enabled) {
return@forEach
}
val url = Utils.idnToASCII(it.second.url)
if (!Utils.isValidUrl(url)) {
return@forEach
}
Log.d(ANG_PACKAGE, url)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val configText = try { val count = mainViewModel.updateConfigViaSubAll()
Utils.getUrlContentWithCustomUserAgent(url) delay(500L)
} catch (e: Exception) {
e.printStackTrace()
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
toast("\"" + it.second.remarks + "\" " + getString(R.string.toast_failure)) if (count > 0) {
toast(R.string.toast_success)
mainViewModel.reloadServerList()
} else {
toast(R.string.toast_failure)
} }
return@launch //dialog.dismiss()
binding.pbWaiting.hide()
} }
launch(Dispatchers.Main) {
importBatchConfig(configText, it.first)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
return false
} }
return true return true
} }
@@ -611,15 +650,18 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/** /**
* import customize config * import customize config
*/ */
fun importCustomizeConfig(server: String?) { private fun importCustomizeConfig(server: String?) {
try { try {
if (server == null || TextUtils.isEmpty(server)) { if (server == null || TextUtils.isEmpty(server)) {
toast(R.string.toast_none_data) toast(R.string.toast_none_data)
return return
} }
mainViewModel.appendCustomConfigServer(server) if (mainViewModel.appendCustomConfigServer(server)) {
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
toast(R.string.toast_success) toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
//adapter.notifyItemInserted(mainViewModel.serverList.lastIndex) //adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
} catch (e: Exception) { } catch (e: Exception) {
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show() ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
@@ -628,7 +670,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
} }
fun setTestState(content: String?) { private fun setTestState(content: String?) {
binding.tvTestState.text = content binding.tvTestState.text = content
} }
@@ -649,27 +691,34 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
return super.onKeyDown(keyCode, event) return super.onKeyDown(keyCode, event)
} }
override fun onNavigationItemSelected(item: MenuItem): Boolean { override fun onNavigationItemSelected(item: MenuItem): Boolean {
// Handle navigation view item clicks here. // Handle navigation view item clicks here.
when (item.itemId) { when (item.itemId) {
//R.id.server_profile -> activityClass = MainActivity::class.java
R.id.sub_setting -> { R.id.sub_setting -> {
startActivity(Intent(this, SubSettingActivity::class.java)) requestSubSettingActivity.launch(Intent(this, SubSettingActivity::class.java))
} }
R.id.settings -> { R.id.settings -> {
startActivity(Intent(this, SettingsActivity::class.java) startActivity(
.putExtra("isRunning", mainViewModel.isRunning.value == true)) Intent(this, SettingsActivity::class.java)
.putExtra("isRunning", mainViewModel.isRunning.value == true)
)
} }
R.id.user_asset_setting -> { R.id.user_asset_setting -> {
startActivity(Intent(this, UserAssetActivity::class.java)) startActivity(Intent(this, UserAssetActivity::class.java))
} }
R.id.promotion -> { R.id.promotion -> {
Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}") Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
} }
R.id.logcat -> { R.id.logcat -> {
startActivity(Intent(this, LogcatActivity::class.java)) startActivity(Intent(this, LogcatActivity::class.java))
} }
R.id.about-> {
R.id.about -> {
startActivity(Intent(this, AboutActivity::class.java)) startActivity(Intent(this, AboutActivity::class.java))
} }
} }

View File

@@ -10,7 +10,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.gson.Gson import com.google.gson.Gson
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AngApplication.Companion.application import com.v2ray.ang.AngApplication.Companion.application
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
@@ -26,21 +25,17 @@ import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.AngConfigManager import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import rx.Observable import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import rx.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>() class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
, ItemTouchHelperAdapter {
companion object { companion object {
private const val VIEW_TYPE_ITEM = 1 private const val VIEW_TYPE_ITEM = 1
private const val VIEW_TYPE_FOOTER = 2 private const val VIEW_TYPE_FOOTER = 2
} }
private var mActivity: MainActivity = activity private var mActivity: MainActivity = activity
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
private val share_method: Array<out String> by lazy { private val share_method: Array<out String> by lazy {
mActivity.resources.getStringArray(R.array.share_method) mActivity.resources.getStringArray(R.array.share_method)
} }
@@ -51,7 +46,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
if (holder is MainViewHolder) { if (holder is MainViewHolder) {
val guid = mActivity.mainViewModel.serversCache[position].guid val guid = mActivity.mainViewModel.serversCache[position].guid
val config = mActivity.mainViewModel.serversCache[position].config val profile = mActivity.mainViewModel.serversCache[position].profile
// //filter // //filter
// if (mActivity.mainViewModel.subscriptionId.isNotEmpty() // if (mActivity.mainViewModel.subscriptionId.isNotEmpty()
// && mActivity.mainViewModel.subscriptionId != config.subscriptionId // && mActivity.mainViewModel.subscriptionId != config.subscriptionId
@@ -61,43 +56,46 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
// holder.itemMainBinding.cardView.visibility = View.VISIBLE // holder.itemMainBinding.cardView.visibility = View.VISIBLE
// } // }
val outbound = config.getProxyOutbound()
val aff = MmkvManager.decodeServerAffiliationInfo(guid) val aff = MmkvManager.decodeServerAffiliationInfo(guid)
holder.itemMainBinding.tvName.text = config.remarks holder.itemMainBinding.tvName.text = profile.remarks
holder.itemView.setBackgroundColor(Color.TRANSPARENT) holder.itemView.setBackgroundColor(Color.TRANSPARENT)
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString() ?: "" holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
if (aff?.testDelayMillis ?: 0L < 0L) { if ((aff?.testDelayMillis ?: 0L) < 0L) {
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed)) holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
} else { } else {
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing)) holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
} }
if (guid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) { if (guid == MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent) holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent)
} else { } else {
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0) holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
} }
holder.itemMainBinding.tvSubscription.text = "" holder.itemMainBinding.tvSubscription.text = ""
val json = subStorage?.decodeString(config.subscriptionId) val json = MmkvManager.subStorage?.decodeString(profile.subscriptionId)
if (!json.isNullOrBlank()) { if (!json.isNullOrBlank()) {
val sub = Gson().fromJson(json, SubscriptionItem::class.java) val sub = Gson().fromJson(json, SubscriptionItem::class.java)
holder.itemMainBinding.tvSubscription.text = sub.remarks holder.itemMainBinding.tvSubscription.text = sub.remarks
} }
var shareOptions = share_method.asList() var shareOptions = share_method.asList()
when (config.configType) { when (profile.configType) {
EConfigType.CUSTOM -> { EConfigType.CUSTOM -> {
holder.itemMainBinding.tvType.text = mActivity.getString(R.string.server_customize_config) holder.itemMainBinding.tvType.text = mActivity.getString(R.string.server_customize_config)
shareOptions = shareOptions.takeLast(1) shareOptions = shareOptions.takeLast(1)
} }
EConfigType.VLESS -> { EConfigType.VLESS -> {
holder.itemMainBinding.tvType.text = config.configType.name holder.itemMainBinding.tvType.text = profile.configType.name
} }
else -> { else -> {
holder.itemMainBinding.tvType.text = config.configType.name.lowercase() holder.itemMainBinding.tvType.text = profile.configType.name.lowercase()
} }
} }
val strState = "${outbound?.getServerAddress()?.dropLast(3)}*** : ${outbound?.getServerPort()}"
val strState = "${profile.server?.dropLast(3)}*** : ${profile.serverPort}"
holder.itemMainBinding.tvStatistics.text = strState holder.itemMainBinding.tvStatistics.text = strState
holder.itemMainBinding.layoutShare.setOnClickListener { holder.itemMainBinding.layoutShare.setOnClickListener {
@@ -105,7 +103,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
try { try {
when (i) { when (i) {
0 -> { 0 -> {
if (config.configType == EConfigType.CUSTOM) { if (profile.configType == EConfigType.CUSTOM) {
shareFullContent(guid) shareFullContent(guid)
} else { } else {
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity)) val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
@@ -113,6 +111,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
AlertDialog.Builder(mActivity).setView(ivBinding.root).show() AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
} }
} }
1 -> { 1 -> {
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) { if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
mActivity.toast(R.string.toast_success) mActivity.toast(R.string.toast_success)
@@ -120,6 +119,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
mActivity.toast(R.string.toast_failure) mActivity.toast(R.string.toast_failure)
} }
} }
2 -> shareFullContent(guid) 2 -> shareFullContent(guid)
else -> mActivity.toast("else") else -> mActivity.toast("else")
} }
@@ -132,20 +132,20 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
holder.itemMainBinding.layoutEdit.setOnClickListener { holder.itemMainBinding.layoutEdit.setOnClickListener {
val intent = Intent().putExtra("guid", guid) val intent = Intent().putExtra("guid", guid)
.putExtra("isRunning", isRunning) .putExtra("isRunning", isRunning)
if (config.configType == EConfigType.CUSTOM) { if (profile.configType == EConfigType.CUSTOM) {
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java)) mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
} else { } else {
mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java)) mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
} }
} }
holder.itemMainBinding.layoutRemove.setOnClickListener { holder.itemMainBinding.layoutRemove.setOnClickListener {
if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) { if (guid != MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { if (MmkvManager.settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm) AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
removeServer(guid, position) removeServer(guid, position)
} }
.setNegativeButton(android.R.string.no) {_, _ -> .setNegativeButton(android.R.string.no) { _, _ ->
//do noting //do noting
} }
.show() .show()
@@ -158,11 +158,11 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
} }
holder.itemMainBinding.infoContainer.setOnClickListener { holder.itemMainBinding.infoContainer.setOnClickListener {
val selected = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) val selected = MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
if (guid != selected) { if (guid != selected) {
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid) MmkvManager.mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
if (!TextUtils.isEmpty(selected)) { if (!TextUtils.isEmpty(selected)) {
notifyItemChanged(mActivity.mainViewModel.getPosition(selected!!)) notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty()))
} }
notifyItemChanged(mActivity.mainViewModel.getPosition(guid)) notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
if (isRunning) { if (isRunning) {
@@ -196,7 +196,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
} }
} }
private fun removeServer(guid: String,position:Int) { private fun removeServer(guid: String, position: Int) {
mActivity.mainViewModel.removeServer(guid) mActivity.mainViewModel.removeServer(guid)
notifyItemRemoved(position) notifyItemRemoved(position)
notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size) notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size)
@@ -206,6 +206,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
return when (viewType) { return when (viewType) {
VIEW_TYPE_ITEM -> VIEW_TYPE_ITEM ->
MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false)) MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false))
else -> else ->
FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false)) FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false))
} }
@@ -237,7 +238,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
override fun onItemDismiss(position: Int) { override fun onItemDismiss(position: Int) {
val guid = mActivity.mainViewModel.serversCache.getOrNull(position)?.guid ?: return val guid = mActivity.mainViewModel.serversCache.getOrNull(position)?.guid ?: return
if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) { if (guid != MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
// mActivity.alert(R.string.del_config_comfirm) { // mActivity.alert(R.string.del_config_comfirm) {
// positiveButton(android.R.string.ok) { // positiveButton(android.R.string.ok) {
mActivity.mainViewModel.removeServer(guid) mActivity.mainViewModel.removeServer(guid)

View File

@@ -21,14 +21,16 @@ import com.v2ray.ang.extension.v2RayApplication
import com.v2ray.ang.util.AppManagerUtil import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import java.text.Collator import java.text.Collator
class PerAppProxyActivity : BaseActivity() { class PerAppProxyActivity : BaseActivity() {
private lateinit var binding: ActivityBypassListBinding private val binding by lazy {
ActivityBypassListBinding.inflate(layoutInflater)
}
private var adapter: PerAppProxyAdapter? = null private var adapter: PerAppProxyAdapter? = null
private var appsAll: List<AppInfo>? = null private var appsAll: List<AppInfo>? = null
@@ -36,9 +38,7 @@ class PerAppProxyActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityBypassListBinding.inflate(layoutInflater) setContentView(binding.root)
val view = binding.root
setContentView(view)
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL) val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
binding.recyclerView.addItemDecoration(dividerItemDecoration) binding.recyclerView.addItemDecoration(dividerItemDecoration)
@@ -188,12 +188,10 @@ class PerAppProxyActivity : BaseActivity() {
if (searchItem != null) { if (searchItem != null) {
val searchView = searchItem.actionView as SearchView val searchView = searchItem.actionView as SearchView
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean = false
return false
}
override fun onQueryTextChange(newText: String?): Boolean { override fun onQueryTextChange(newText: String?): Boolean {
filterProxyApp(newText!!) filterProxyApp(newText.orEmpty())
return false return false
} }
}) })
@@ -209,29 +207,33 @@ class PerAppProxyActivity : BaseActivity() {
if (it.blacklist.containsAll(pkgNames)) { if (it.blacklist.containsAll(pkgNames)) {
it.apps.forEach { it.apps.forEach {
val packageName = it.packageName val packageName = it.packageName
adapter?.blacklist!!.remove(packageName) adapter?.blacklist?.remove(packageName)
} }
} else { } else {
it.apps.forEach { it.apps.forEach {
val packageName = it.packageName val packageName = it.packageName
adapter?.blacklist!!.add(packageName) adapter?.blacklist?.add(packageName)
} }
} }
it.notifyDataSetChanged() it.notifyDataSetChanged()
true true
} ?: false } ?: false
R.id.select_proxy_app -> { R.id.select_proxy_app -> {
selectProxyApp() selectProxyApp()
true true
} }
R.id.import_proxy_app -> { R.id.import_proxy_app -> {
importProxyApp() importProxyApp()
true true
} }
R.id.export_proxy_app -> { R.id.export_proxy_app -> {
exportProxyApp() exportProxyApp()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
@@ -250,9 +252,7 @@ class PerAppProxyActivity : BaseActivity() {
private fun importProxyApp() { private fun importProxyApp() {
val content = Utils.getClipboard(applicationContext) val content = Utils.getClipboard(applicationContext)
if (TextUtils.isEmpty(content)) { if (TextUtils.isEmpty(content)) return
return
}
selectProxyApp(content, false) selectProxyApp(content, false)
toast(R.string.toast_success) toast(R.string.toast_success)
} }
@@ -274,11 +274,9 @@ class PerAppProxyActivity : BaseActivity() {
} else { } else {
content content
} }
if (TextUtils.isEmpty(proxyApps)) { if (TextUtils.isEmpty(proxyApps)) return false
return false
}
adapter?.blacklist!!.clear() adapter?.blacklist?.clear()
if (binding.switchBypassApps.isChecked) { if (binding.switchBypassApps.isChecked) {
adapter?.let { adapter?.let {
@@ -286,7 +284,7 @@ class PerAppProxyActivity : BaseActivity() {
val packageName = it.packageName val packageName = it.packageName
Log.d(ANG_PACKAGE, packageName) Log.d(ANG_PACKAGE, packageName)
if (!inProxyApps(proxyApps, packageName, force)) { if (!inProxyApps(proxyApps, packageName, force)) {
adapter?.blacklist!!.add(packageName) adapter?.blacklist?.add(packageName)
println(packageName) println(packageName)
return@block return@block
} }
@@ -299,7 +297,7 @@ class PerAppProxyActivity : BaseActivity() {
val packageName = it.packageName val packageName = it.packageName
Log.d(ANG_PACKAGE, packageName) Log.d(ANG_PACKAGE, packageName)
if (inProxyApps(proxyApps, packageName, force)) { if (inProxyApps(proxyApps, packageName, force)) {
adapter?.blacklist!!.add(packageName) adapter?.blacklist?.add(packageName)
println(packageName) println(packageName)
return@block return@block
} }
@@ -316,12 +314,8 @@ class PerAppProxyActivity : BaseActivity() {
private fun inProxyApps(proxyApps: String, packageName: String, force: Boolean): Boolean { private fun inProxyApps(proxyApps: String, packageName: String, force: Boolean): Boolean {
if (force) { if (force) {
if (packageName == "com.google.android.webview") { if (packageName == "com.google.android.webview") return false
return false if (packageName.startsWith("com.google")) return true
}
if (packageName.startsWith("com.google")) {
return true
}
} }
return proxyApps.indexOf(packageName) >= 0 return proxyApps.indexOf(packageName) >= 0
@@ -334,7 +328,8 @@ class PerAppProxyActivity : BaseActivity() {
if (key.isNotEmpty()) { if (key.isNotEmpty()) {
appsAll?.forEach { appsAll?.forEach {
if (it.appName.uppercase().indexOf(key) >= 0 if (it.appName.uppercase().indexOf(key) >= 0
|| it.packageName.uppercase().indexOf(key) >= 0) { || it.packageName.uppercase().indexOf(key) >= 0
) {
apps.add(it) apps.add(it)
} }
} }

View File

@@ -1,13 +1,12 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.recyclerview.widget.RecyclerView
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
import com.v2ray.ang.dto.AppInfo import com.v2ray.ang.dto.AppInfo
import java.util.*
class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, blacklist: MutableSet<String>?) : class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, blacklist: MutableSet<String>?) :
RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() { RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() {
@@ -34,8 +33,10 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
return when (viewType) { return when (viewType) {
VIEW_TYPE_HEADER -> { VIEW_TYPE_HEADER -> {
val view = View(ctx) val view = View(ctx)
view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, view.layoutParams = ViewGroup.LayoutParams(
ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 0) ViewGroup.LayoutParams.MATCH_PARENT,
ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 0
)
BaseViewHolder(view) BaseViewHolder(view)
} }
// VIEW_TYPE_ITEM -> AppViewHolder(ctx.layoutInflater // VIEW_TYPE_ITEM -> AppViewHolder(ctx.layoutInflater

View File

@@ -1,14 +1,14 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.os.Bundle import android.os.Bundle
import com.v2ray.ang.R
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingSettingsBinding import com.v2ray.ang.databinding.ActivityRoutingSettingsBinding
class RoutingSettingsActivity : BaseActivity() { class RoutingSettingsActivity : BaseActivity() {
private lateinit var binding: ActivityRoutingSettingsBinding private val binding by lazy { ActivityRoutingSettingsBinding.inflate(layoutInflater) }
private val titles: Array<out String> by lazy { private val titles: Array<out String> by lazy {
resources.getStringArray(R.array.routing_tag) resources.getStringArray(R.array.routing_tag)
@@ -16,9 +16,7 @@ class RoutingSettingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityRoutingSettingsBinding.inflate(layoutInflater) setContentView(binding.root)
val view = binding.root
setContentView(view)
title = getString(R.string.title_pref_routing_custom) title = getString(R.string.title_pref_routing_custom)

View File

@@ -1,16 +1,20 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.Manifest import android.Manifest
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.view.* import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import com.tbruyelle.rxpermissions3.RxPermissions
import com.tbruyelle.rxpermissions.RxPermissions
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
@@ -23,17 +27,19 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class RoutingSettingsFragment : Fragment() { class RoutingSettingsFragment : Fragment() {
private lateinit var binding: FragmentRoutingSettingsBinding private val binding by lazy { FragmentRoutingSettingsBinding.inflate(layoutInflater) }
companion object { companion object {
private const val routing_arg = "routing_arg" private const val routing_arg = "routing_arg"
} }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, override fun onCreateView(
savedInstanceState: Bundle?): View? { inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment // Inflate the layout for this fragment
binding = FragmentRoutingSettingsBinding.inflate(layoutInflater)
return binding.root// inflater.inflate(R.layout.fragment_routing_settings, container, false) return binding.root// inflater.inflate(R.layout.fragment_routing_settings, container, false)
} }
@@ -49,7 +55,7 @@ class RoutingSettingsFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val content = settingsStorage?.getString(requireArguments().getString(routing_arg), "") val content = settingsStorage?.getString(requireArguments().getString(routing_arg), "")
binding.etRoutingContent.text = Utils.getEditable(content!!) binding.etRoutingContent.text = Utils.getEditable(content)
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
@@ -64,22 +70,27 @@ class RoutingSettingsFragment : Fragment() {
saveRouting() saveRouting()
true true
} }
R.id.del_routing -> { R.id.del_routing -> {
binding.etRoutingContent.text = null binding.etRoutingContent.text = null
true true
} }
R.id.scan_replace -> { R.id.scan_replace -> {
scanQRcode(true) scanQRcode(true)
true true
} }
R.id.scan_append -> { R.id.scan_append -> {
scanQRcode(false) scanQRcode(false)
true true
} }
R.id.default_rules -> { R.id.default_rules -> {
setDefaultRules() setDefaultRules()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
@@ -113,7 +124,7 @@ class RoutingSettingsFragment : Fragment() {
private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) { if (it.resultCode == RESULT_OK) {
val content = it.data?.getStringExtra("SCAN_RESULT") val content = it.data?.getStringExtra("SCAN_RESULT")
binding.etRoutingContent.text = Utils.getEditable(content!!) binding.etRoutingContent.text = Utils.getEditable(content)
} }
} }
@@ -131,9 +142,11 @@ class RoutingSettingsFragment : Fragment() {
AppConfig.PREF_V2RAY_ROUTING_AGENT -> { AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
tag = AppConfig.TAG_PROXY tag = AppConfig.TAG_PROXY
} }
AppConfig.PREF_V2RAY_ROUTING_DIRECT -> { AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
tag = AppConfig.TAG_DIRECT tag = AppConfig.TAG_DIRECT
} }
AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> { AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> {
tag = AppConfig.TAG_BLOCKED tag = AppConfig.TAG_BLOCKED
} }
@@ -145,7 +158,7 @@ class RoutingSettingsFragment : Fragment() {
val content = Utils.getUrlContext(url, 5000) val content = Utils.getUrlContext(url, 5000)
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
val routingList = if (TextUtils.isEmpty(content)) { val routingList = if (TextUtils.isEmpty(content)) {
Utils.readTextFromAssets(activity?.v2RayApplication!!, "custom_routing_$tag") Utils.readTextFromAssets(activity?.v2RayApplication, "custom_routing_$tag")
} else { } else {
content content
} }

View File

@@ -1,13 +1,13 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.Manifest import android.Manifest
import android.content.* import android.content.Intent
import com.tbruyelle.rxpermissions.RxPermissions
import com.v2ray.ang.R
import com.v2ray.ang.util.AngConfigManager
import android.os.Bundle import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.R
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager
class ScScannerActivity : BaseActivity() { class ScScannerActivity : BaseActivity() {
@@ -32,8 +32,8 @@ class ScScannerActivity : BaseActivity() {
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) { if (it.resultCode == RESULT_OK) {
val count = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false) val (count, countSub) = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false)
if (count > 0) { if (count + countSub > 0) {
toast(R.string.toast_success) toast(R.string.toast_success)
} else { } else {
toast(R.string.toast_failure) toast(R.string.toast_failure)

View File

@@ -1,9 +1,9 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import com.v2ray.ang.R
import com.v2ray.ang.util.Utils
import android.os.Bundle import android.os.Bundle
import com.v2ray.ang.R
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.Utils
class ScSwitchActivity : BaseActivity() { class ScSwitchActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

View File

@@ -1,15 +1,14 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.Manifest import android.Manifest
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Build import android.os.Build
import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import com.tbruyelle.rxpermissions.RxPermissions import com.tbruyelle.rxpermissions3.RxPermissions
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
@@ -20,7 +19,7 @@ import io.github.g00fy2.quickie.QRResult
import io.github.g00fy2.quickie.ScanCustomCode import io.github.g00fy2.quickie.ScanCustomCode
import io.github.g00fy2.quickie.config.ScannerConfig import io.github.g00fy2.quickie.config.ScannerConfig
class ScannerActivity : BaseActivity(){ class ScannerActivity : BaseActivity() {
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult) private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
@@ -33,7 +32,7 @@ class ScannerActivity : BaseActivity(){
} }
} }
private fun launchScan(){ private fun launchScan() {
scanQrCode.launch( scanQrCode.launch(
ScannerConfig.build { ScannerConfig.build {
setHapticSuccessFeedback(true) // enable (default) or disable haptic feedback when a barcode was detected setHapticSuccessFeedback(true) // enable (default) or disable haptic feedback when a barcode was detected
@@ -44,8 +43,8 @@ class ScannerActivity : BaseActivity(){
} }
private fun handleResult(result: QRResult) { private fun handleResult(result: QRResult) {
if (result is QRResult.QRSuccess ) { if (result is QRResult.QRSuccess) {
finished(result.content.rawValue!!) finished(result.content.rawValue.orEmpty())
} else { } else {
finish() finish()
} }
@@ -54,7 +53,7 @@ class ScannerActivity : BaseActivity(){
private fun finished(text: String) { private fun finished(text: String) {
val intent = Intent() val intent = Intent()
intent.putExtra("SCAN_RESULT", text) intent.putExtra("SCAN_RESULT", text)
setResult(AppCompatActivity.RESULT_OK, intent) setResult(RESULT_OK, intent)
finish() finish()
} }
@@ -68,6 +67,7 @@ class ScannerActivity : BaseActivity(){
launchScan() launchScan()
true true
} }
R.id.select_photo -> { R.id.select_photo -> {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES Manifest.permission.READ_MEDIA_IMAGES
@@ -88,6 +88,7 @@ class ScannerActivity : BaseActivity(){
} }
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
@@ -110,7 +111,7 @@ class ScannerActivity : BaseActivity(){
try { try {
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri)) val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
val text = QRCodeDecoder.syncDecodeQRCode(bitmap) val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
finished(text!!) finished(text.orEmpty())
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
toast(e.message.toString()) toast(e.message.toString())

View File

@@ -5,10 +5,14 @@ import android.text.TextUtils
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.* import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
@@ -106,7 +110,9 @@ class ServerActivity : BaseActivity() {
private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) } private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) }
private val sp_header_type: Spinner? by lazy { findViewById(R.id.sp_header_type) } private val sp_header_type: Spinner? by lazy { findViewById(R.id.sp_header_type) }
private val sp_header_type_title: TextView? by lazy { findViewById(R.id.sp_header_type_title) } private val sp_header_type_title: TextView? by lazy { findViewById(R.id.sp_header_type_title) }
private val tv_request_host: TextView? by lazy { findViewById(R.id.tv_request_host) }
private val et_request_host: EditText? by lazy { findViewById(R.id.et_request_host) } private val et_request_host: EditText? by lazy { findViewById(R.id.et_request_host) }
private val tv_path: TextView? by lazy { findViewById(R.id.tv_path) }
private val et_path: EditText? by lazy { findViewById(R.id.et_path) } private val et_path: EditText? by lazy { findViewById(R.id.et_path) }
private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
private val container_alpn: LinearLayout? by lazy { findViewById(R.id.l4) } private val container_alpn: LinearLayout? by lazy { findViewById(R.id.l4) }
@@ -158,6 +164,36 @@ class ServerActivity : BaseActivity() {
et_request_host?.text = Utils.getEditable(transportDetails[1]) et_request_host?.text = Utils.getEditable(transportDetails[1])
et_path?.text = Utils.getEditable(transportDetails[2]) et_path?.text = Utils.getEditable(transportDetails[2])
} }
tv_request_host?.text = Utils.getEditable(
getString(
when (networks[position]) {
"tcp" -> R.string.server_lab_request_host_http
"ws" -> R.string.server_lab_request_host_ws
"httpupgrade" -> R.string.server_lab_request_host_httpupgrade
"splithttp" -> R.string.server_lab_request_host_splithttp
"h2" -> R.string.server_lab_request_host_h2
"quic" -> R.string.server_lab_request_host_quic
"grpc" -> R.string.server_lab_request_host_grpc
else -> R.string.server_lab_request_host
}
)
)
tv_path?.text = Utils.getEditable(
getString(
when (networks[position]) {
"kcp" -> R.string.server_lab_path_kcp
"ws" -> R.string.server_lab_path_ws
"httpupgrade" -> R.string.server_lab_path_httpupgrade
"splithttp" -> R.string.server_lab_path_splithttp
"h2" -> R.string.server_lab_path_h2
"quic" -> R.string.server_lab_path_quic
"grpc" -> R.string.server_lab_path_grpc
else -> R.string.server_lab_path
}
)
)
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
@@ -286,7 +322,7 @@ class ServerActivity : BaseActivity() {
tlsSetting.alpn?.let { tlsSetting.alpn?.let {
val alpnIndex = Utils.arrayFind( val alpnIndex = Utils.arrayFind(
alpns, alpns,
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString())!! Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
) )
sp_stream_alpn?.setSelection(alpnIndex) sp_stream_alpn?.setSelection(alpnIndex)
} }
@@ -414,7 +450,7 @@ class ServerActivity : BaseActivity() {
saveStreamSettings(it) saveStreamSettings(it)
} }
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) { if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
config.subscriptionId = subscriptionId!! config.subscriptionId = subscriptionId.orEmpty()
} }
MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerConfig(editGuid, config)

View File

@@ -8,7 +8,7 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.blacksquircle.ui.editorkit.utils.EditorTheme import com.blacksquircle.ui.editorkit.utils.EditorTheme
import com.blacksquircle.ui.language.json.JsonLanguage import com.blacksquircle.ui.language.json.JsonLanguage
import com.google.gson.* import com.google.gson.Gson
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
@@ -21,7 +21,7 @@ import com.v2ray.ang.util.Utils
import me.drakeet.support.toast.ToastCompat import me.drakeet.support.toast.ToastCompat
class ServerCustomConfigActivity : BaseActivity() { class ServerCustomConfigActivity : BaseActivity() {
private lateinit var binding: ActivityServerCustomConfigBinding private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) }
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) } private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) } private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
@@ -34,9 +34,7 @@ class ServerCustomConfigActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityServerCustomConfigBinding.inflate(layoutInflater) setContentView(binding.root)
val view = binding.root
setContentView(view)
title = getString(R.string.title_server) title = getString(R.string.title_server)
if (!Utils.getDarkModeStatus(this)) { if (!Utils.getDarkModeStatus(this)) {
@@ -91,7 +89,7 @@ class ServerCustomConfigActivity : BaseActivity() {
} }
val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM) val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM)
config.remarks = v2rayConfig.remarks ?: binding.etRemarks.text.toString().trim() config.remarks = if (binding.etRemarks.text.isNullOrEmpty()) v2rayConfig.remarks.orEmpty() else binding.etRemarks.text.toString()
config.fullConfig = v2rayConfig config.fullConfig = v2rayConfig
MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerConfig(editGuid, config)
@@ -111,7 +109,7 @@ class ServerCustomConfigActivity : BaseActivity() {
MmkvManager.removeServer(editGuid) MmkvManager.removeServer(editGuid)
finish() finish()
} }
.setNegativeButton(android.R.string.no) {_, _ -> .setNegativeButton(android.R.string.no) { _, _ ->
// do nothing // do nothing
} }
.show() .show()
@@ -141,10 +139,12 @@ class ServerCustomConfigActivity : BaseActivity() {
deleteServer() deleteServer()
true true
} }
R.id.save_config -> { R.id.save_config -> {
saveServer() saveServer()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }

View File

@@ -63,6 +63,7 @@ class SettingsActivity : BaseActivity() {
private val httpPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HTTP_PORT) } private val httpPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HTTP_PORT) }
private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) } private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) }
private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_DNS) } private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_DNS) }
private val delayTestUrl by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DELAY_TEST_URL) }
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) } private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
override fun onCreatePreferences(bundle: Bundle?, s: String?) { override fun onCreatePreferences(bundle: Bundle?, s: String?) {
@@ -143,18 +144,6 @@ class SettingsActivity : BaseActivity() {
true true
} }
remoteDns?.setOnPreferenceChangeListener { _, any ->
// remoteDns.summary = any as String
val nval = any as String
remoteDns?.summary = if (nval == "") AppConfig.DNS_PROXY else nval
true
}
domesticDns?.setOnPreferenceChangeListener { _, any ->
// domesticDns.summary = any as String
val nval = any as String
domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
true
}
socksPort?.setOnPreferenceChangeListener { _, any -> socksPort?.setOnPreferenceChangeListener { _, any ->
val nval = any as String val nval = any as String
socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval
@@ -165,6 +154,21 @@ class SettingsActivity : BaseActivity() {
httpPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_HTTP else nval httpPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_HTTP else nval
true true
} }
remoteDns?.setOnPreferenceChangeListener { _, any ->
val nval = any as String
remoteDns?.summary = if (nval == "") AppConfig.DNS_PROXY else nval
true
}
domesticDns?.setOnPreferenceChangeListener { _, any ->
val nval = any as String
domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
true
}
delayTestUrl?.setOnPreferenceChangeListener { _, any ->
val nval = any as String
delayTestUrl?.summary = if (nval == "") AppConfig.DelayTestUrl else nval
true
}
mode?.setOnPreferenceChangeListener { _, newValue -> mode?.setOnPreferenceChangeListener { _, newValue ->
updateMode(newValue.toString()) updateMode(newValue.toString())
true true
@@ -194,13 +198,15 @@ class SettingsActivity : BaseActivity() {
fragmentInterval?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20") fragmentInterval?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
autoUpdateCheck?.isChecked = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) autoUpdateCheck?.isChecked = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
autoUpdateInterval?.summary = settingsStorage.decodeString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL,AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL) autoUpdateInterval?.summary =
settingsStorage.decodeString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
autoUpdateInterval?.isEnabled = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) autoUpdateInterval?.isEnabled = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
socksPort?.summary = settingsStorage.decodeString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS) socksPort?.summary = settingsStorage.decodeString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
httpPort?.summary = settingsStorage.decodeString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP) httpPort?.summary = settingsStorage.decodeString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
remoteDns?.summary = settingsStorage.decodeString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY) remoteDns?.summary = settingsStorage.decodeString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
domesticDns?.summary = settingsStorage.decodeString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT) domesticDns?.summary = settingsStorage.decodeString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
delayTestUrl?.summary = settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl)
initSharedPreference() initSharedPreference()
} }
@@ -217,7 +223,8 @@ class SettingsActivity : BaseActivity() {
socksPort, socksPort,
httpPort, httpPort,
remoteDns, remoteDns,
domesticDns domesticDns,
delayTestUrl
).forEach { key -> ).forEach { key ->
key?.text = key?.summary.toString() key?.text = key?.summary.toString()
} }
@@ -230,6 +237,7 @@ class SettingsActivity : BaseActivity() {
} }
listOf( listOf(
AppConfig.PREF_ROUTE_ONLY_ENABLED,
AppConfig.PREF_BYPASS_APPS, AppConfig.PREF_BYPASS_APPS,
AppConfig.PREF_SPEED_ENABLED, AppConfig.PREF_SPEED_ENABLED,
AppConfig.PREF_CONFIRM_REMOVE, AppConfig.PREF_CONFIRM_REMOVE,
@@ -343,12 +351,15 @@ class SettingsActivity : BaseActivity() {
updateFragmentInterval(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")) updateFragmentInterval(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
} }
} }
private fun updateFragmentPackets(value: String?) { private fun updateFragmentPackets(value: String?) {
fragmentPackets?.summary = value.toString() fragmentPackets?.summary = value.toString()
} }
private fun updateFragmentLength(value: String?) { private fun updateFragmentLength(value: String?) {
fragmentLength?.summary = value.toString() fragmentLength?.summary = value.toString()
} }
private fun updateFragmentInterval(value: String?) { private fun updateFragmentInterval(value: String?) {
fragmentInterval?.summary = value.toString() fragmentInterval?.summary = value.toString()
} }

View File

@@ -5,25 +5,20 @@ import android.text.TextUtils
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.work.Constraints import androidx.lifecycle.lifecycleScope
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.multiprocess.RemoteWorkManager
import com.google.gson.Gson import com.google.gson.Gson
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.AngApplication
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubEditBinding import com.v2ray.ang.databinding.ActivitySubEditBinding
import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.service.SubscriptionUpdater
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class SubEditActivity : BaseActivity() { class SubEditActivity : BaseActivity() {
private lateinit var binding: ActivitySubEditBinding private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) }
var del_config: MenuItem? = null var del_config: MenuItem? = null
var save_config: MenuItem? = null var save_config: MenuItem? = null
@@ -33,9 +28,7 @@ class SubEditActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivitySubEditBinding.inflate(layoutInflater) setContentView(binding.root)
val view = binding.root
setContentView(view)
title = getString(R.string.title_sub_setting) title = getString(R.string.title_sub_setting)
val json = subStorage?.decodeString(editSubId) val json = subStorage?.decodeString(editSubId)
@@ -108,10 +101,14 @@ class SubEditActivity : BaseActivity() {
if (editSubId.isNotEmpty()) { if (editSubId.isNotEmpty()) {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch(Dispatchers.IO) {
MmkvManager.removeSubscription(editSubId) MmkvManager.removeSubscription(editSubId)
launch(Dispatchers.Main) {
finish() finish()
} }
.setNegativeButton(android.R.string.no) {_, _ -> }
}
.setNegativeButton(android.R.string.no) { _, _ ->
// do nothing // do nothing
} }
.show() .show()
@@ -136,10 +133,12 @@ class SubEditActivity : BaseActivity() {
deleteServer() deleteServer()
true true
} }
R.id.save_config -> { R.id.save_config -> {
saveServer() saveServer()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }

View File

@@ -1,26 +1,32 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.content.Intent import android.content.Intent
import androidx.recyclerview.widget.LinearLayoutManager import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.v2ray.ang.R import com.v2ray.ang.R
import android.os.Bundle
import com.v2ray.ang.databinding.ActivitySubSettingBinding import com.v2ray.ang.databinding.ActivitySubSettingBinding
import com.v2ray.ang.databinding.LayoutProgressBinding
import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SubSettingActivity : BaseActivity() { class SubSettingActivity : BaseActivity() {
private lateinit var binding: ActivitySubSettingBinding private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
var subscriptions:List<Pair<String, SubscriptionItem>> = listOf() var subscriptions: List<Pair<String, SubscriptionItem>> = listOf()
private val adapter by lazy { SubSettingRecyclerAdapter(this) } private val adapter by lazy { SubSettingRecyclerAdapter(this) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivitySubSettingBinding.inflate(layoutInflater) setContentView(binding.root)
val view = binding.root
setContentView(view)
title = getString(R.string.title_sub_setting) title = getString(R.string.title_sub_setting)
@@ -37,9 +43,6 @@ class SubSettingActivity : BaseActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.action_sub_setting, menu) menuInflater.inflate(R.menu.action_sub_setting, menu)
menu.findItem(R.id.del_config)?.isVisible = false
menu.findItem(R.id.save_config)?.isVisible = false
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
} }
@@ -48,6 +51,30 @@ class SubSettingActivity : BaseActivity() {
startActivity(Intent(this, SubEditActivity::class.java)) startActivity(Intent(this, SubEditActivity::class.java))
true true
} }
R.id.sub_update -> {
val dialog = AlertDialog.Builder(this)
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
.setCancelable(false)
.show()
lifecycleScope.launch(Dispatchers.IO) {
val count = AngConfigManager.updateConfigViaSubAll()
delay(500L)
launch(Dispatchers.Main) {
if (count > 0) {
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
dialog.dismiss()
}
}
true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }

View File

@@ -1,23 +1,21 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.ArrayAdapter
import android.widget.ListView
import java.util.ArrayList
import com.v2ray.ang.R
import android.content.Intent import android.content.Intent
import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import android.widget.ArrayAdapter
import android.widget.ListView
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityTaskerBinding import com.v2ray.ang.databinding.ActivityTaskerBinding
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager
class TaskerActivity : BaseActivity() { class TaskerActivity : BaseActivity() {
private lateinit var binding: ActivityTaskerBinding private val binding by lazy { ActivityTaskerBinding.inflate(layoutInflater) }
private var listview: ListView? = null private var listview: ListView? = null
private var lstData: ArrayList<String> = ArrayList() private var lstData: ArrayList<String> = ArrayList()
@@ -27,9 +25,7 @@ class TaskerActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityTaskerBinding.inflate(layoutInflater) setContentView(binding.root)
val view = binding.root
setContentView(view)
//add def value //add def value
lstData.add("Default") lstData.add("Default")
@@ -41,10 +37,12 @@ class TaskerActivity : BaseActivity() {
lstGuid.add(key) lstGuid.add(key)
} }
} }
val adapter = ArrayAdapter(this, val adapter = ArrayAdapter(
android.R.layout.simple_list_item_single_choice, lstData) this,
android.R.layout.simple_list_item_single_choice, lstData
)
listview = findViewById<View>(R.id.listview) as ListView listview = findViewById<View>(R.id.listview) as ListView
listview!!.adapter = adapter listview?.adapter = adapter
init() init()
} }
@@ -90,7 +88,7 @@ class TaskerActivity : BaseActivity() {
intent.putExtra(AppConfig.TASKER_EXTRA_BUNDLE, extraBundle) intent.putExtra(AppConfig.TASKER_EXTRA_BUNDLE, extraBundle)
intent.putExtra(AppConfig.TASKER_EXTRA_STRING_BLURB, blurb) intent.putExtra(AppConfig.TASKER_EXTRA_STRING_BLURB, blurb)
setResult(AppCompatActivity.RESULT_OK, intent) setResult(RESULT_OK, intent)
finish() finish()
} }
@@ -105,10 +103,12 @@ class TaskerActivity : BaseActivity() {
R.id.del_config -> { R.id.del_config -> {
true true
} }
R.id.save_config -> { R.id.save_config -> {
confirmFinish() confirmFinish()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }

View File

@@ -3,7 +3,7 @@ package com.v2ray.ang.ui
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import com.v2ray.ang.AppConfig import android.util.Log
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityLogcatBinding import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
@@ -11,45 +11,32 @@ import com.v2ray.ang.util.AngConfigManager
import java.net.URLDecoder import java.net.URLDecoder
class UrlSchemeActivity : BaseActivity() { class UrlSchemeActivity : BaseActivity() {
private lateinit var binding: ActivityLogcatBinding private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityLogcatBinding.inflate(layoutInflater) setContentView(binding.root)
val view = binding.root
setContentView(view)
try { try {
intent.apply { intent.apply {
if (action == Intent.ACTION_SEND) { if (action == Intent.ACTION_SEND) {
if ("text/plain" == type) { if ("text/plain" == type) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
val uri = Uri.parse(it) parseUri(it, null)
if (uri.scheme?.startsWith(AppConfig.PROTOCOL_HTTPS) == true || uri.scheme?.startsWith(
AppConfig.PROTOCOL_HTTP
) == true
) {
val name = uri.getQueryParameter("name") ?: "Subscription"
importSubscription(it, name)
} else {
importConfig(it)
}
} }
} }
} else if (action == Intent.ACTION_VIEW) { } else if (action == Intent.ACTION_VIEW) {
when (data?.host) { when (data?.host) {
"install-config" -> { "install-config" -> {
val uri: Uri? = intent.data val uri: Uri? = intent.data
val shareUrl: String = uri?.getQueryParameter("url")!! val shareUrl = uri?.getQueryParameter("url").orEmpty()
toast(shareUrl) parseUri(shareUrl, uri?.fragment)
importConfig(shareUrl)
} }
"install-sub" -> { "install-sub" -> {
val uri: Uri? = intent.data val uri: Uri? = intent.data
val url = uri?.getQueryParameter("url")!! val shareUrl = uri?.getQueryParameter("url").orEmpty()
val name = uri.getQueryParameter("name") ?: "Subscription" parseUri(shareUrl, uri?.fragment)
importSubscription(url, name)
} }
else -> { else -> {
@@ -57,10 +44,8 @@ class UrlSchemeActivity : BaseActivity() {
} }
} }
} }
} }
startActivity(Intent(this, MainActivity::class.java)) startActivity(Intent(this, MainActivity::class.java))
finish() finish()
} catch (e: Exception) { } catch (e: Exception) {
@@ -68,19 +53,25 @@ class UrlSchemeActivity : BaseActivity() {
} }
} }
private fun importSubscription(url: String, name: String) { private fun parseUri(uriString: String?, fragment: String?) {
val decodedUrl = URLDecoder.decode(url, "UTF-8") if (uriString.isNullOrEmpty()) {
return
val check = AngConfigManager.importSubscription(name, decodedUrl)
if (check) toast(R.string.import_subscription_success) else toast(R.string.import_subscription_failure)
} }
Log.d("UrlScheme", uriString)
private fun importConfig(shareUrl: String) { var decodedUrl = URLDecoder.decode(uriString, "UTF-8")
val count = AngConfigManager.importBatchConfig(shareUrl, "", false) val uri = Uri.parse(decodedUrl)
if (count > 0) { if (uri != null) {
toast(R.string.toast_success) if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) {
decodedUrl += "#${fragment}"
}
Log.d("UrlScheme-decodedUrl", decodedUrl)
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
if (count + countSub > 0) {
toast(R.string.import_subscription_success)
} else { } else {
toast(R.string.toast_failure) toast(R.string.import_subscription_failure)
}
} }
} }
} }

View File

@@ -2,26 +2,31 @@ package com.v2ray.ang.ui
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.util.Log import android.util.Log
import android.view.* import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.gson.Gson import com.google.gson.Gson
import com.tbruyelle.rxpermissions.RxPermissions import com.tbruyelle.rxpermissions3.RxPermissions
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubSettingBinding import com.v2ray.ang.databinding.ActivitySubSettingBinding
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
import com.v2ray.ang.databinding.LayoutProgressBinding
import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.toTrafficString import com.v2ray.ang.extension.toTrafficString
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
@@ -36,10 +41,10 @@ import java.net.InetSocketAddress
import java.net.Proxy import java.net.Proxy
import java.net.URL import java.net.URL
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.Date
class UserAssetActivity : BaseActivity() { class UserAssetActivity : BaseActivity() {
private lateinit var binding: ActivitySubSettingBinding private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) } private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
@@ -49,9 +54,7 @@ class UserAssetActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivitySubSettingBinding.inflate(layoutInflater) setContentView(binding.root)
val view = binding.root
setContentView(view)
title = getString(R.string.title_user_asset_setting) title = getString(R.string.title_user_asset_setting)
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
@@ -80,6 +83,7 @@ class UserAssetActivity : BaseActivity() {
startActivity(intent) startActivity(intent)
true true
} }
R.id.download_file -> { R.id.download_file -> {
downloadGeoFiles() downloadGeoFiles()
true true
@@ -168,9 +172,13 @@ class UserAssetActivity : BaseActivity() {
} }
private fun downloadGeoFiles() { private fun downloadGeoFiles() {
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt()) val dialog = AlertDialog.Builder(this)
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
.setCancelable(false)
.show()
toast(R.string.msg_downloading_content) toast(R.string.msg_downloading_content)
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
var assets = MmkvManager.decodeAssetUrls() var assets = MmkvManager.decodeAssetUrls()
assets = addBuiltInGeoItems(assets) assets = addBuiltInGeoItems(assets)
@@ -188,6 +196,7 @@ class UserAssetActivity : BaseActivity() {
} else { } else {
toast(getString(R.string.toast_failure) + " " + it.second.remarks) toast(getString(R.string.toast_failure) + " " + it.second.remarks)
} }
dialog.dismiss()
} }
} }
} }
@@ -229,15 +238,18 @@ class UserAssetActivity : BaseActivity() {
conn?.disconnect() conn?.disconnect()
} }
} }
private fun addBuiltInGeoItems(assets: List<Pair<String, AssetUrlItem>>): List<Pair<String, AssetUrlItem>> { private fun addBuiltInGeoItems(assets: List<Pair<String, AssetUrlItem>>): List<Pair<String, AssetUrlItem>> {
val list = mutableListOf<Pair<String, AssetUrlItem>>() val list = mutableListOf<Pair<String, AssetUrlItem>>()
builtInGeoFiles builtInGeoFiles
.filter { geoFile -> assets.none { it.second.remarks == geoFile } } .filter { geoFile -> assets.none { it.second.remarks == geoFile } }
.forEach { .forEach {
list.add(Utils.getUuid() to AssetUrlItem( list.add(
Utils.getUuid() to AssetUrlItem(
it, it,
AppConfig.GeoUrl + it AppConfig.GeoUrl + it
)) )
)
} }
return list + assets return list + assets
@@ -249,14 +261,15 @@ class UserAssetActivity : BaseActivity() {
ItemRecyclerUserAssetBinding.inflate( ItemRecyclerUserAssetBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,
false) false
)
) )
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) { override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
var assets = MmkvManager.decodeAssetUrls(); var assets = MmkvManager.decodeAssetUrls()
assets = addBuiltInGeoItems(assets); assets = addBuiltInGeoItems(assets)
val item = assets.getOrNull(position) ?: return val item = assets.getOrNull(position) ?: return
// file with name == item.second.remarks // file with name == item.second.remarks
val file = extDir.listFiles()?.find { it.name == item.second.remarks } val file = extDir.listFiles()?.find { it.name == item.second.remarks }
@@ -292,8 +305,8 @@ class UserAssetActivity : BaseActivity() {
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
var assets = MmkvManager.decodeAssetUrls(); var assets = MmkvManager.decodeAssetUrls()
assets = addBuiltInGeoItems(assets); assets = addBuiltInGeoItems(assets)
return assets.size return assets.size
} }
} }

View File

@@ -16,7 +16,7 @@ import com.v2ray.ang.util.Utils
import java.io.File import java.io.File
class UserAssetUrlActivity : BaseActivity() { class UserAssetUrlActivity : BaseActivity() {
private lateinit var binding: ActivityUserAssetUrlBinding private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
var del_config: MenuItem? = null var del_config: MenuItem? = null
var save_config: MenuItem? = null var save_config: MenuItem? = null
@@ -27,9 +27,7 @@ class UserAssetUrlActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityUserAssetUrlBinding.inflate(layoutInflater) setContentView(binding.root)
val view = binding.root
setContentView(view)
title = getString(R.string.title_user_asset_add_url) title = getString(R.string.title_user_asset_add_url)
val json = assetStorage?.decodeString(editAssetId) val json = assetStorage?.decodeString(editAssetId)
@@ -114,7 +112,7 @@ class UserAssetUrlActivity : BaseActivity() {
MmkvManager.removeAssetUrl(editAssetId) MmkvManager.removeAssetUrl(editAssetId)
finish() finish()
} }
.setNegativeButton(android.R.string.no) {_, _ -> .setNegativeButton(android.R.string.no) { _, _ ->
// do nothing // do nothing
} }
.show() .show()
@@ -139,10 +137,12 @@ class UserAssetUrlActivity : BaseActivity() {
deleteServer() deleteServer()
true true
} }
R.id.save_config -> { R.id.save_config -> {
saveServer() saveServer()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }

View File

@@ -12,19 +12,16 @@ import com.google.gson.JsonSerializer
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.PROTOCOL_HTTP
import com.v2ray.ang.AppConfig.PROTOCOL_HTTPS
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.dto.* import com.v2ray.ang.dto.*
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_SECURITY
import com.v2ray.ang.dto.V2rayConfig.Companion.TLS
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
import com.v2ray.ang.util.fmt.ShadowsocksFmt
import com.v2ray.ang.util.fmt.SocksFmt
import com.v2ray.ang.util.fmt.TrojanFmt
import com.v2ray.ang.util.fmt.VlessFmt
import com.v2ray.ang.util.fmt.VmessFmt
import com.v2ray.ang.util.fmt.WireguardFmt
import java.lang.reflect.Type import java.lang.reflect.Type
import java.net.URI
import java.util.* import java.util.*
object AngConfigManager { object AngConfigManager {
@@ -205,9 +202,9 @@ object AngConfigManager {
// } // }
/** /**
* import config form qrcode or... * parse config form qrcode or...
*/ */
private fun importConfig( private fun parseConfig(
str: String?, str: String?,
subid: String, subid: String,
removedSelectedServer: ServerConfig? removedSelectedServer: ServerConfig?
@@ -217,254 +214,22 @@ object AngConfigManager {
return R.string.toast_none_data return R.string.toast_none_data
} }
//maybe sub val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
if (TextUtils.isEmpty(subid) && (str.startsWith(PROTOCOL_HTTP) || str.startsWith( VmessFmt.parseVmess(str)
PROTOCOL_HTTPS
))
) {
MmkvManager.importUrlAsSubscription(str)
return 0
}
var config: ServerConfig? = null
val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
config = ServerConfig.create(EConfigType.VMESS)
val streamSetting = config.outboundBean?.streamSettings ?: return -1
if (!tryParseNewVmess(str, config, allowInsecure)) {
if (str.indexOf("?") > 0) {
if (!tryResolveVmess4Kitsunebi(str, config)) {
return R.string.toast_incorrect_protocol
}
} else {
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
result = Utils.decode(result)
if (TextUtils.isEmpty(result)) {
return R.string.toast_decoding_failed
}
val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java)
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
if (TextUtils.isEmpty(vmessQRCode.add)
|| TextUtils.isEmpty(vmessQRCode.port)
|| TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net)
) {
return R.string.toast_incorrect_protocol
}
config.remarks = vmessQRCode.ps
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = vmessQRCode.add
vnext.port = Utils.parseInt(vmessQRCode.port)
vnext.users[0].id = vmessQRCode.id
vnext.users[0].security =
if (TextUtils.isEmpty(vmessQRCode.scy)) DEFAULT_SECURITY else vmessQRCode.scy
vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid)
}
val sni = streamSetting.populateTransportSettings(
vmessQRCode.net,
vmessQRCode.type,
vmessQRCode.host,
vmessQRCode.path,
vmessQRCode.path,
vmessQRCode.host,
vmessQRCode.path,
vmessQRCode.type,
vmessQRCode.path,
vmessQRCode.host
)
val fingerprint = vmessQRCode.fp ?: streamSetting.tlsSettings?.fingerprint
streamSetting.populateTlsSettings(
vmessQRCode.tls, allowInsecure,
if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni,
fingerprint, vmessQRCode.alpn, null, null, null
)
}
}
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) { } else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
config = ServerConfig.create(EConfigType.SHADOWSOCKS) ShadowsocksFmt.parseShadowsocks(str)
if (!tryResolveResolveSip002(str, config)) {
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
val indexSplit = result.indexOf("#")
if (indexSplit > 0) {
try {
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
result = if (indexS > 0) {
Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
Utils.decode(result)
}
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
val match = legacyPattern.matchEntire(result)
?: return R.string.toast_incorrect_protocol
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = match.groupValues[3].removeSurrounding("[", "]")
server.port = match.groupValues[4].toInt()
server.password = match.groupValues[2]
server.method = match.groupValues[1].lowercase()
}
}
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) { } else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
var result = str.replace(EConfigType.SOCKS.protocolScheme, "") SocksFmt.parseSocks(str)
val indexSplit = result.indexOf("#")
config = ServerConfig.create(EConfigType.SOCKS)
if (indexSplit > 0) {
try {
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
if (indexS > 0) {
result = Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
result = Utils.decode(result)
}
val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex()
val match =
legacyPattern.matchEntire(result) ?: return R.string.toast_incorrect_protocol
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = match.groupValues[3].removeSurrounding("[", "]")
server.port = match.groupValues[4].toInt()
val socksUsersBean =
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = match.groupValues[1]
socksUsersBean.pass = match.groupValues[2]
server.users = listOf(socksUsersBean)
}
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) { } else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
val uri = URI(Utils.fixIllegalUrl(str)) TrojanFmt.parseTrojan(str)
config = ServerConfig.create(EConfigType.TROJAN)
config.remarks = Utils.urlDecode(uri.fragment ?: "")
var flow = ""
var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint
if (uri.rawQuery != null) {
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val sni = config.outboundBean?.streamSettings?.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"]
)
fingerprint = queryParam["fp"] ?: ""
config.outboundBean?.streamSettings?.populateTlsSettings(
queryParam["security"] ?: TLS,
allowInsecure, queryParam["sni"] ?: sni!!, fingerprint, queryParam["alpn"],
null, null, null
)
flow = queryParam["flow"] ?: ""
} else {
config.outboundBean?.streamSettings?.populateTlsSettings(
TLS, allowInsecure, "",
fingerprint, null, null, null, null
)
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = uri.idnHost
server.port = uri.port
server.password = uri.userInfo
server.flow = flow
}
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) { } else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
val uri = URI(Utils.fixIllegalUrl(str)) VlessFmt.parseVless(str)
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
config = ServerConfig.create(EConfigType.VLESS)
val streamSetting = config.outboundBean?.streamSettings ?: return -1
config.remarks = Utils.urlDecode(uri.fragment ?: "")
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = uri.idnHost
vnext.port = uri.port
vnext.users[0].id = uri.userInfo
vnext.users[0].encryption = queryParam["encryption"] ?: "none"
vnext.users[0].flow = queryParam["flow"] ?: ""
}
val sni = streamSetting.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"]
)
streamSetting.populateTlsSettings(
queryParam["security"] ?: "",
allowInsecure,
queryParam["sni"] ?: sni,
queryParam["fp"] ?: "",
queryParam["alpn"],
queryParam["pbk"] ?: "",
queryParam["sid"] ?: "",
queryParam["spx"] ?: ""
)
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) { } else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
val uri = URI(Utils.fixIllegalUrl(str)) WireguardFmt.parseWireguard(str)
config = ServerConfig.create(EConfigType.WIREGUARD) } else {
config.remarks = Utils.urlDecode(uri.fragment ?: "") null
}
if (uri.rawQuery != null) {
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
config.outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = uri.userInfo
wireguard.address =
(queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace()
.split(",")
wireguard.peers?.get(0)?.publicKey = queryParam["publickey"] ?: ""
wireguard.peers?.get(0)?.endpoint = "${uri.idnHost}:${uri.port}"
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: WIREGUARD_LOCAL_MTU)
wireguard.reserved =
(queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",")
.map { it.toInt() }
}
}
}
if (config == null) { if (config == null) {
return R.string.toast_incorrect_protocol return R.string.toast_incorrect_protocol
} }
@@ -487,383 +252,21 @@ object AngConfigManager {
return 0 return 0
} }
private fun tryParseNewVmess(
uriString: String,
config: ServerConfig,
allowInsecure: Boolean
): Boolean {
return runCatching {
val uri = URI(Utils.fixIllegalUrl(uriString))
check(uri.scheme == "vmess")
val (_, protocol, tlsStr, uuid, alterId) =
Regex("(tcp|http|ws|kcp|quic|grpc)(\\+tls)?:([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})")
.matchEntire(uri.userInfo)?.groupValues
?: error("parse user info fail.")
val tls = tlsStr.isNotBlank()
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val streamSetting = config.outboundBean?.streamSettings ?: return false
config.remarks = Utils.urlDecode(uri.fragment ?: "")
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = uri.idnHost
vnext.port = uri.port
vnext.users[0].id = uuid
vnext.users[0].security = DEFAULT_SECURITY
vnext.users[0].alterId = alterId.toInt()
}
var fingerprint = streamSetting.tlsSettings?.fingerprint
val sni = streamSetting.populateTransportSettings(protocol,
queryParam["type"],
queryParam["host"]?.split("|")?.get(0) ?: "",
queryParam["path"]?.takeIf { it.trim() != "/" } ?: "",
queryParam["seed"],
queryParam["security"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"])
streamSetting.populateTlsSettings(
if (tls) TLS else "", allowInsecure, sni, fingerprint, null,
null, null, null
)
true
}.getOrElse { false }
}
private fun tryResolveVmess4Kitsunebi(server: String, config: ServerConfig): Boolean {
var result = server.replace(EConfigType.VMESS.protocolScheme, "")
val indexSplit = result.indexOf("?")
if (indexSplit > 0) {
result = result.substring(0, indexSplit)
}
result = Utils.decode(result)
val arr1 = result.split('@')
if (arr1.count() != 2) {
return false
}
val arr21 = arr1[0].split(':')
val arr22 = arr1[1].split(':')
if (arr21.count() != 2) {
return false
}
config.remarks = "Alien"
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = arr22[0]
vnext.port = Utils.parseInt(arr22[1])
vnext.users[0].id = arr21[1]
vnext.users[0].security = arr21[0]
vnext.users[0].alterId = 0
}
return true
}
private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {
try {
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment ?: "")
val method: String
val password: String
if (uri.userInfo.contains(":")) {
val arrUserInfo = uri.userInfo.split(":").map { it.trim() }
if (arrUserInfo.count() != 2) {
return false
}
method = arrUserInfo[0]
password = Utils.urlDecode(arrUserInfo[1])
} else {
val base64Decode = Utils.decode(uri.userInfo)
val arrUserInfo = base64Decode.split(":").map { it.trim() }
if (arrUserInfo.count() < 2) {
return false
}
method = arrUserInfo[0]
password = base64Decode.substringAfter(":")
}
val query = Utils.urlDecode(uri.query ?: "")
if (query != "") {
val queryPairs = HashMap<String, String>()
val pairs = query.split(";")
Log.d(AppConfig.ANG_PACKAGE, pairs.toString())
for (pair in pairs) {
val idx = pair.indexOf("=")
if (idx == -1) {
queryPairs[Utils.urlDecode(pair)] = "";
} else {
queryPairs[Utils.urlDecode(pair.substring(0, idx))] =
Utils.urlDecode(pair.substring(idx + 1))
}
}
Log.d(AppConfig.ANG_PACKAGE, queryPairs.toString())
var sni: String? = ""
if (queryPairs["plugin"] == "obfs-local" && queryPairs["obfs"] == "http") {
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
"tcp",
"http",
queryPairs["obfs-host"],
queryPairs["path"],
null,
null,
null,
null,
null,
null
)
} else if (queryPairs["plugin"] == "v2ray-plugin") {
var network = "ws";
if (queryPairs["mode"] == "quic") {
network = "quic";
}
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
network,
null,
queryPairs["host"],
queryPairs["path"],
null,
null,
null,
null,
null,
null
)
}
if ("tls" in queryPairs) {
config.outboundBean?.streamSettings?.populateTlsSettings(
"tls", false, sni ?: "", null, null, null, null, null
)
}
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = uri.idnHost
server.port = uri.port
server.password = password
server.method = method
}
return true
} catch (e: Exception) {
Log.d(AppConfig.ANG_PACKAGE, e.toString())
return false
}
}
/** /**
* share config * share config
*/ */
private fun shareConfig(guid: String): String { private fun shareConfig(guid: String): String {
try { try {
val config = MmkvManager.decodeServerConfig(guid) ?: return "" val config = MmkvManager.decodeServerConfig(guid) ?: return ""
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting =
outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
if (config.configType != EConfigType.WIREGUARD) {
if (outbound.streamSettings == null) return ""
}
return config.configType.protocolScheme + when (config.configType) { return config.configType.protocolScheme + when (config.configType) {
EConfigType.VMESS -> { EConfigType.VMESS -> VmessFmt.toUri(config)
val vmessQRCode = VmessQRCode()
vmessQRCode.v = "2"
vmessQRCode.ps = config.remarks
vmessQRCode.add = outbound.getServerAddress().orEmpty()
vmessQRCode.port = outbound.getServerPort().toString()
vmessQRCode.id = outbound.getPassword().orEmpty()
vmessQRCode.aid =
outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()
vmessQRCode.scy =
outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString()
vmessQRCode.net = streamSetting.network
vmessQRCode.tls = streamSetting.security
vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty()
vmessQRCode.alpn =
Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString())
.orEmpty()
vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty()
outbound.getTransportSettingDetails()?.let { transportDetails ->
vmessQRCode.type = transportDetails[0]
vmessQRCode.host = transportDetails[1]
vmessQRCode.path = transportDetails[2]
}
val json = Gson().toJson(vmessQRCode)
Utils.encode(json)
}
EConfigType.CUSTOM -> "" EConfigType.CUSTOM -> ""
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
EConfigType.SHADOWSOCKS -> { EConfigType.SOCKS -> SocksFmt.toUri(config)
val remark = "#" + Utils.urlEncode(config.remarks) EConfigType.VLESS -> VlessFmt.toUri(config)
val pw = EConfigType.TROJAN -> TrojanFmt.toUri(config)
Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}") EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
val url = String.format(
"%s@%s:%s",
pw,
Utils.getIpv6Address(outbound.getServerAddress()!!),
outbound.getServerPort()
)
url + remark
}
EConfigType.SOCKS -> {
val remark = "#" + Utils.urlEncode(config.remarks)
val pw =
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()
)
url + remark
}
EConfigType.VLESS,
EConfigType.TROJAN -> {
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
if (config.configType == EConfigType.VLESS) {
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let {
if (!TextUtils.isEmpty(it)) {
dicQuery["flow"] = it
}
}
dicQuery["encryption"] =
if (outbound.getSecurityEncryption().isNullOrEmpty()) "none"
else outbound.getSecurityEncryption().orEmpty()
} else if (config.configType == EConfigType.TROJAN) {
config.outboundBean?.settings?.servers?.get(0)?.flow?.let {
if (!TextUtils.isEmpty(it)) {
dicQuery["flow"] = it
}
}
}
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
(streamSetting.tlsSettings
?: streamSetting.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!!
}
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
dicQuery["pbk"] = tlsSetting.publicKey!!
}
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
dicQuery["sid"] = tlsSetting.shortId!!
}
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX!!)
}
}
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" -> {
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()
)
url + query + remark
}
EConfigType.WIREGUARD -> {
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
dicQuery["publickey"] =
Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString())
if (outbound.settings?.reserved != null) {
dicQuery["reserved"] = Utils.urlEncode(
Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString())
.toString()
)
}
dicQuery["address"] = Utils.urlEncode(
Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString())
.toString()
)
if (outbound.settings?.mtu != null) {
dicQuery["mtu"] = outbound.settings?.mtu.toString()
}
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()
)
url + query + remark
}
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -983,7 +386,47 @@ object AngConfigManager {
// } // }
// } // }
fun importBatchConfig(servers: String?, subid: String, append: Boolean): Int { fun importBatchConfig(server: String?, subid: String, append: Boolean): Pair<Int, Int> {
var count = parseBatchConfig(Utils.decode(server), subid, append)
if (count <= 0) {
count = parseBatchConfig(server, subid, append)
}
if (count <= 0) {
count = parseCustomConfigServer(server, subid)
}
var countSub = parseBatchSubscription(server)
if (countSub <= 0) {
countSub = parseBatchSubscription(Utils.decode(server))
}
if (countSub > 0) {
updateConfigViaSubAll()
}
return count to countSub
}
fun parseBatchSubscription(servers: String?): Int {
try {
if (servers == null) {
return 0
}
var count = 0
servers.lines()
.forEach { str ->
if (str.startsWith(AppConfig.PROTOCOL_HTTP) || str.startsWith(AppConfig.PROTOCOL_HTTPS)) {
count += MmkvManager.importUrlAsSubscription(str)
}
}
return count
} catch (e: Exception) {
e.printStackTrace()
}
return 0
}
fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
try { try {
if (servers == null) { if (servers == null) {
return 0 return 0
@@ -991,7 +434,7 @@ object AngConfigManager {
val removedSelectedServer = val removedSelectedServer =
if (!TextUtils.isEmpty(subid) && !append) { if (!TextUtils.isEmpty(subid) && !append) {
MmkvManager.decodeServerConfig( MmkvManager.decodeServerConfig(
mainStorage?.decodeString(KEY_SELECTED_SERVER) ?: "" mainStorage?.decodeString(KEY_SELECTED_SERVER).orEmpty()
)?.let { )?.let {
if (it.subscriptionId == subid) { if (it.subscriptionId == subid) {
return@let it return@let it
@@ -1004,16 +447,12 @@ object AngConfigManager {
if (!append) { if (!append) {
MmkvManager.removeServerViaSubid(subid) MmkvManager.removeServerViaSubid(subid)
} }
// var servers = server
// if (server.indexOf("vmess") >= 0 && server.indexOf("vmess") == server.lastIndexOf("vmess")) {
// servers = server.replace("\n", "")
// }
var count = 0 var count = 0
servers.lines() servers.lines()
.reversed() .reversed()
.forEach { .forEach {
val resId = importConfig(it, subid, removedSelectedServer) val resId = parseConfig(it, subid, removedSelectedServer)
if (resId == 0) { if (resId == 0) {
count++ count++
} }
@@ -1025,24 +464,7 @@ object AngConfigManager {
return 0 return 0
} }
fun importSubscription(remark: String, url: String, enabled: Boolean = true): Boolean { fun parseCustomConfigServer(server: String?, subid: String): Int {
val subId = Utils.getUuid()
val subItem = SubscriptionItem()
subItem.remarks = remark
subItem.url = url
subItem.enabled = enabled
if (TextUtils.isEmpty(subItem.remarks) || TextUtils.isEmpty(subItem.url)) {
return false
}
subStorage?.encode(subId, Gson().toJson(subItem))
return true
}
fun appendCustomConfigServer(server: String?, subid: String): Int {
if (server == null) { if (server == null) {
return 0 return 0
} }
@@ -1069,9 +491,10 @@ object AngConfigManager {
if (serverList.isNotEmpty()) { if (serverList.isNotEmpty()) {
var count = 0 var count = 0
for (srv in serverList) { for (srv in serverList.reversed()) {
val config = ServerConfig.create(EConfigType.CUSTOM) val config = ServerConfig.create(EConfigType.CUSTOM)
config.fullConfig = Gson().fromJson(Gson().toJson(srv), V2rayConfig::class.java) config.fullConfig =
Gson().fromJson(Gson().toJson(srv), V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks config.remarks = config.fullConfig?.remarks
?: ("%04d-".format(count + 1) + System.currentTimeMillis() ?: ("%04d-".format(count + 1) + System.currentTimeMillis()
.toString()) .toString())
@@ -1094,8 +517,83 @@ object AngConfigManager {
val key = MmkvManager.encodeServerConfig("", config) val key = MmkvManager.encodeServerConfig("", config)
serverRawStorage?.encode(key, server) serverRawStorage?.encode(key, server)
return 1 return 1
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
val config = WireguardFmt.parseWireguardConfFile(server)
?: return R.string.toast_incorrect_protocol
config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
val key = MmkvManager.encodeServerConfig("", config)
serverRawStorage?.encode(key, server)
return 1
} else { } else {
return 0 return 0
} }
} }
fun updateConfigViaSubAll(): Int {
var count = 0
try {
MmkvManager.decodeSubscriptions().forEach {
count += updateConfigViaSub(it)
}
} catch (e: Exception) {
e.printStackTrace()
return 0
}
return count
}
fun updateConfigViaSub(it: Pair<String, SubscriptionItem>): Int {
try {
if (TextUtils.isEmpty(it.first)
|| TextUtils.isEmpty(it.second.remarks)
|| TextUtils.isEmpty(it.second.url)
) {
return 0
}
if (!it.second.enabled) {
return 0
}
val url = Utils.idnToASCII(it.second.url)
if (!Utils.isValidUrl(url)) {
return 0
}
Log.d(AppConfig.ANG_PACKAGE, url)
var configText = try {
Utils.getUrlContentWithCustomUserAgent(url)
} catch (e: Exception) {
e.printStackTrace()
""
}
if (configText.isEmpty()) {
configText = try {
val httpPort = Utils.parseInt(
settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT),
AppConfig.PORT_HTTP.toInt()
)
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
} catch (e: Exception) {
e.printStackTrace()
""
}
}
if (configText.isEmpty()) {
return 0
}
return parseConfigViaSub(configText, it.first, false)
} catch (e: Exception) {
e.printStackTrace()
return 0
}
}
private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int {
var count = parseBatchConfig(Utils.decode(server), subid, append)
if (count <= 0) {
count = parseBatchConfig(server, subid, append)
}
if (count <= 0) {
count = parseCustomConfigServer(server, subid)
}
return count
}
} }

View File

@@ -6,8 +6,7 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import com.v2ray.ang.dto.AppInfo import com.v2ray.ang.dto.AppInfo
import rx.Observable import io.reactivex.rxjava3.core.Observable
import java.util.*
object AppManagerUtil { object AppManagerUtil {
fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> { fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> {
@@ -31,7 +30,8 @@ object AppManagerUtil {
return apps return apps
} }
fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> = Observable.unsafeCreate { fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> =
Observable.unsafeCreate {
it.onNext(loadNetworkAppList(ctx)) it.onNext(loadNetworkAppList(ctx))
} }

View File

@@ -3,6 +3,7 @@ package com.v2ray.ang.util
import com.google.gson.Gson import com.google.gson.Gson
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.ServerAffiliationInfo import com.v2ray.ang.dto.ServerAffiliationInfo
import com.v2ray.ang.dto.ServerConfig import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.dto.SubscriptionItem
@@ -11,6 +12,7 @@ import java.net.URI
object MmkvManager { object MmkvManager {
const val ID_MAIN = "MAIN" const val ID_MAIN = "MAIN"
const val ID_SERVER_CONFIG = "SERVER_CONFIG" const val ID_SERVER_CONFIG = "SERVER_CONFIG"
const val ID_PROFILE_CONFIG = "PROFILE_CONFIG"
const val ID_SERVER_RAW = "SERVER_RAW" const val ID_SERVER_RAW = "SERVER_RAW"
const val ID_SERVER_AFF = "SERVER_AFF" const val ID_SERVER_AFF = "SERVER_AFF"
const val ID_SUB = "SUB" const val ID_SUB = "SUB"
@@ -19,11 +21,14 @@ object MmkvManager {
const val KEY_SELECTED_SERVER = "SELECTED_SERVER" const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
const val KEY_ANG_CONFIGS = "ANG_CONFIGS" const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) } val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
val settingsStorage by lazy { MMKV.mmkvWithID(ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) } private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) }
private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) } private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) }
private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) } val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) } private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
fun decodeServerList(): MutableList<String> { fun decodeServerList(): MutableList<String> {
val json = mainStorage?.decodeString(KEY_ANG_CONFIGS) val json = mainStorage?.decodeString(KEY_ANG_CONFIGS)
@@ -45,6 +50,17 @@ object MmkvManager {
return Gson().fromJson(json, ServerConfig::class.java) return Gson().fromJson(json, ServerConfig::class.java)
} }
fun decodeProfileConfig(guid: String): ProfileItem? {
if (guid.isBlank()) {
return null
}
val json = profileStorage?.decodeString(guid)
if (json.isNullOrBlank()) {
return null
}
return Gson().fromJson(json, ProfileItem::class.java)
}
fun encodeServerConfig(guid: String, config: ServerConfig): String { fun encodeServerConfig(guid: String, config: ServerConfig): String {
val key = guid.ifBlank { Utils.getUuid() } val key = guid.ifBlank { Utils.getUuid() }
serverStorage?.encode(key, Gson().toJson(config)) serverStorage?.encode(key, Gson().toJson(config))
@@ -56,6 +72,14 @@ object MmkvManager {
mainStorage?.encode(KEY_SELECTED_SERVER, key) mainStorage?.encode(KEY_SELECTED_SERVER, key)
} }
} }
val profile = ProfileItem(
configType = config.configType,
subscriptionId = config.subscriptionId,
remarks = config.remarks,
server = config.getProxyOutbound()?.getServerAddress(),
serverPort = config.getProxyOutbound()?.getServerPort(),
)
profileStorage?.encode(key, Gson().toJson(profile))
return key return key
} }
@@ -70,6 +94,7 @@ object MmkvManager {
serverList.remove(guid) serverList.remove(guid)
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList)) mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
serverStorage?.remove(guid) serverStorage?.remove(guid)
profileStorage?.remove(guid)
serverAffStorage?.remove(guid) serverAffStorage?.remove(guid)
} }
@@ -106,8 +131,8 @@ object MmkvManager {
serverAffStorage?.encode(guid, Gson().toJson(aff)) serverAffStorage?.encode(guid, Gson().toJson(aff))
} }
fun clearAllTestDelayResults() { fun clearAllTestDelayResults(keys: List<String>?) {
serverAffStorage?.allKeys()?.forEach { key -> keys?.forEach { key ->
decodeServerAffiliationInfo(key)?.let { aff -> decodeServerAffiliationInfo(key)?.let { aff ->
aff.testDelayMillis = 0 aff.testDelayMillis = 0
serverAffStorage?.encode(key, Gson().toJson(aff)) serverAffStorage?.encode(key, Gson().toJson(aff))
@@ -124,7 +149,7 @@ object MmkvManager {
} }
val uri = URI(Utils.fixIllegalUrl(url)) val uri = URI(Utils.fixIllegalUrl(url))
val subItem = SubscriptionItem() val subItem = SubscriptionItem()
subItem.remarks = Utils.urlDecode(uri.fragment ?: "import sub") subItem.remarks = uri.fragment ?: "import sub"
subItem.url = url subItem.url = url
subStorage?.encode(Utils.getUuid(), Gson().toJson(subItem)) subStorage?.encode(Utils.getUuid(), Gson().toJson(subItem))
return 1 return 1
@@ -138,8 +163,7 @@ object MmkvManager {
subscriptions.add(Pair(key, Gson().fromJson(json, SubscriptionItem::class.java))) subscriptions.add(Pair(key, Gson().fromJson(json, SubscriptionItem::class.java)))
} }
} }
subscriptions.sortedBy { (_, value) -> value.addedTime } return subscriptions.sortedBy { (_, value) -> value.addedTime }
return subscriptions
} }
fun removeSubscription(subid: String) { fun removeSubscription(subid: String) {
@@ -155,8 +179,7 @@ object MmkvManager {
assetUrlItems.add(Pair(key, Gson().fromJson(json, AssetUrlItem::class.java))) assetUrlItems.add(Pair(key, Gson().fromJson(json, AssetUrlItem::class.java)))
} }
} }
assetUrlItems.sortedBy { (_, value) -> value.addedTime } return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
return assetUrlItems
} }
fun removeAssetUrl(assetid: String) { fun removeAssetUrl(assetid: String) {
@@ -166,20 +189,29 @@ object MmkvManager {
fun removeAllServer() { fun removeAllServer() {
mainStorage?.clearAll() mainStorage?.clearAll()
serverStorage?.clearAll() serverStorage?.clearAll()
profileStorage?.clearAll()
serverAffStorage?.clearAll() serverAffStorage?.clearAll()
} }
fun removeInvalidServer() { fun removeInvalidServer(guid: String) {
if (guid.isNotEmpty()) {
decodeServerAffiliationInfo(guid)?.let { aff ->
if (aff.testDelayMillis < 0L) {
removeServer(guid)
}
}
} else {
serverAffStorage?.allKeys()?.forEach { key -> serverAffStorage?.allKeys()?.forEach { key ->
decodeServerAffiliationInfo(key)?.let { aff -> decodeServerAffiliationInfo(key)?.let { aff ->
if (aff.testDelayMillis <= 0L) { if (aff.testDelayMillis < 0L) {
removeServer(key) removeServer(key)
} }
} }
} }
} }
}
fun sortByTestResults( ) { fun sortByTestResults() {
data class ServerDelay(var guid: String, var testDelayMillis: Long) data class ServerDelay(var guid: String, var testDelayMillis: Long)
val serverDelays = mutableListOf<ServerDelay>() val serverDelays = mutableListOf<ServerDelay>()

View File

@@ -7,7 +7,7 @@ import android.content.res.Resources
import android.os.Build import android.os.Build
import android.os.LocaleList import android.os.LocaleList
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import java.util.* import java.util.Locale
open class MyContextWrapper(base: Context?) : ContextWrapper(base) { open class MyContextWrapper(base: Context?) : ContextWrapper(base) {
companion object { companion object {

View File

@@ -2,11 +2,16 @@ package com.v2ray.ang.util
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import com.google.zxing.* import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType
import com.google.zxing.EncodeHintType
import com.google.zxing.NotFoundException
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.common.GlobalHistogramBinarizer import com.google.zxing.common.GlobalHistogramBinarizer
import com.google.zxing.common.HybridBinarizer import com.google.zxing.qrcode.QRCodeReader
import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.qrcode.QRCodeWriter
import java.util.* import java.util.EnumMap
/** /**
* 描述:解析二维码图片 * 描述:解析二维码图片
@@ -21,8 +26,10 @@ object QRCodeDecoder {
try { try {
val hints = HashMap<EncodeHintType, String>() val hints = HashMap<EncodeHintType, String>()
hints[EncodeHintType.CHARACTER_SET] = "utf-8" hints[EncodeHintType.CHARACTER_SET] = "utf-8"
val bitMatrix = QRCodeWriter().encode(text, val bitMatrix = QRCodeWriter().encode(
BarcodeFormat.QR_CODE, size, size, hints) text,
BarcodeFormat.QR_CODE, size, size, hints
)
val pixels = IntArray(size * size) val pixels = IntArray(size * size)
for (y in 0 until size) { for (y in 0 until size) {
for (x in 0 until size) { for (x in 0 until size) {
@@ -34,8 +41,10 @@ object QRCodeDecoder {
} }
} }
val bitmap = Bitmap.createBitmap(size, size, val bitmap = Bitmap.createBitmap(
Bitmap.Config.ARGB_8888) size, size,
Bitmap.Config.ARGB_8888
)
bitmap.setPixels(pixels, 0, size, 0, 0, size, size) bitmap.setPixels(pixels, 0, size, 0, 0, size, size)
return bitmap return bitmap
} catch (e: Exception) { } catch (e: Exception) {
@@ -61,24 +70,37 @@ object QRCodeDecoder {
* @return 返回二维码图片里的内容 或 null * @return 返回二维码图片里的内容 或 null
*/ */
fun syncDecodeQRCode(bitmap: Bitmap?): String? { fun syncDecodeQRCode(bitmap: Bitmap?): String? {
if (bitmap == null) {
return null
}
var source: RGBLuminanceSource? = null var source: RGBLuminanceSource? = null
try { try {
val width = bitmap!!.width val width = bitmap.width
val height = bitmap.height val height = bitmap.height
val pixels = IntArray(width * height) val pixels = IntArray(width * height)
bitmap.getPixels(pixels, 0, width, 0, 0, width, height) bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
source = RGBLuminanceSource(width, height, pixels) source = RGBLuminanceSource(width, height, pixels)
return MultiFormatReader().decode(BinaryBitmap(HybridBinarizer(source)), HINTS).text 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 result.text
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
if (source != null) { } catch (e: Exception) {
try { e.printStackTrace()
return MultiFormatReader().decode(BinaryBitmap(GlobalHistogramBinarizer(source)), HINTS).text
} catch (e2: Throwable) {
e2.printStackTrace()
}
} }
return null return null
} }
@@ -107,23 +129,24 @@ object QRCodeDecoder {
init { init {
val allFormats: List<BarcodeFormat> = arrayListOf( val allFormats: List<BarcodeFormat> = arrayListOf(
BarcodeFormat.AZTEC BarcodeFormat.AZTEC,
,BarcodeFormat.CODABAR BarcodeFormat.CODABAR,
,BarcodeFormat.CODE_39 BarcodeFormat.CODE_39,
,BarcodeFormat.CODE_93 BarcodeFormat.CODE_93,
,BarcodeFormat.CODE_128 BarcodeFormat.CODE_128,
,BarcodeFormat.DATA_MATRIX BarcodeFormat.DATA_MATRIX,
,BarcodeFormat.EAN_8 BarcodeFormat.EAN_8,
,BarcodeFormat.EAN_13 BarcodeFormat.EAN_13,
,BarcodeFormat.ITF BarcodeFormat.ITF,
,BarcodeFormat.MAXICODE BarcodeFormat.MAXICODE,
,BarcodeFormat.PDF_417 BarcodeFormat.PDF_417,
,BarcodeFormat.QR_CODE BarcodeFormat.QR_CODE,
,BarcodeFormat.RSS_14 BarcodeFormat.RSS_14,
,BarcodeFormat.RSS_EXPANDED BarcodeFormat.RSS_EXPANDED,
,BarcodeFormat.UPC_A BarcodeFormat.UPC_A,
,BarcodeFormat.UPC_E BarcodeFormat.UPC_E,
,BarcodeFormat.UPC_EAN_EXTENSION) BarcodeFormat.UPC_EAN_EXTENSION
)
HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE
HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats
HINTS[DecodeHintType.CHARACTER_SET] = "utf-8" HINTS[DecodeHintType.CHARACTER_SET] = "utf-8"

View File

@@ -10,8 +10,12 @@ import com.v2ray.ang.extension.responseLength
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import libv2ray.Libv2ray import libv2ray.Libv2ray
import java.io.IOException import java.io.IOException
import java.net.* import java.net.HttpURLConnection
import java.util.* import java.net.InetSocketAddress
import java.net.Proxy
import java.net.Socket
import java.net.URL
import java.net.UnknownHostException
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
object SpeedtestUtil { object SpeedtestUtil {
@@ -34,7 +38,7 @@ object SpeedtestUtil {
fun realPing(config: String): Long { fun realPing(config: String): Long {
return try { return try {
Libv2ray.measureOutboundDelay(config) Libv2ray.measureOutboundDelay(config, Utils.getDelayTestUrl())
} catch (e: Exception) { } catch (e: Exception) {
Log.d(AppConfig.ANG_PACKAGE, "realPing: $e") Log.d(AppConfig.ANG_PACKAGE, "realPing: $e")
-1L -1L
@@ -48,7 +52,8 @@ object SpeedtestUtil {
val allText = process.inputStream.bufferedReader().use { it.readText() } val allText = process.inputStream.bufferedReader().use { it.readText() }
if (!TextUtils.isEmpty(allText)) { if (!TextUtils.isEmpty(allText)) {
val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19) val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19)
val temps = tempInfo.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() val temps =
tempInfo.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (temps.count() > 0 && temps[0].length < 10) { if (temps.count() > 0 && temps[0].length < 10) {
return temps[0].toFloat().toInt().toString() + "ms" return temps[0].toFloat().toInt().toString() + "ms"
} }
@@ -66,7 +71,7 @@ object SpeedtestUtil {
tcpTestingSockets.add(socket) tcpTestingSockets.add(socket)
} }
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
socket.connect(InetSocketAddress(url, port),3000) socket.connect(InetSocketAddress(url, port), 3000)
val time = System.currentTimeMillis() - start val time = System.currentTimeMillis() - start
synchronized(this) { synchronized(this) {
tcpTestingSockets.remove(socket) tcpTestingSockets.remove(socket)
@@ -98,13 +103,14 @@ object SpeedtestUtil {
var conn: HttpURLConnection? = null var conn: HttpURLConnection? = null
try { try {
val url = URL("https", val url = URL(Utils.getDelayTestUrl())
"www.google.com",
"/generate_204")
conn = url.openConnection( conn = url.openConnection(
Proxy(Proxy.Type.HTTP, Proxy(
InetSocketAddress("127.0.0.1", port))) as HttpURLConnection Proxy.Type.HTTP,
InetSocketAddress("127.0.0.1", port)
)
) as HttpURLConnection
conn.connectTimeout = 30000 conn.connectTimeout = 30000
conn.readTimeout = 30000 conn.readTimeout = 30000
conn.setRequestProperty("Connection", "close") conn.setRequestProperty("Connection", "close")
@@ -118,11 +124,19 @@ object SpeedtestUtil {
if (code == 204 || code == 200 && conn.responseLength == 0L) { if (code == 204 || code == 200 && conn.responseLength == 0L) {
result = context.getString(R.string.connection_test_available, elapsed) result = context.getString(R.string.connection_test_available, elapsed)
} else { } else {
throw IOException(context.getString(R.string.connection_test_error_status_code, code)) throw IOException(
context.getString(
R.string.connection_test_error_status_code,
code
)
)
} }
} catch (e: IOException) { } catch (e: IOException) {
// network exception // 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) result = context.getString(R.string.connection_test_error, e.message)
} catch (e: Exception) { } catch (e: Exception) {
// library exception, eg sumsung // library exception, eg sumsung

View File

@@ -39,8 +39,8 @@ object Utils {
* @param text * @param text
* @return * @return
*/ */
fun getEditable(text: String): Editable { fun getEditable(text: String?): Editable {
return Editable.Factory.getInstance().newEditable(text) return Editable.Factory.getInstance().newEditable(text.orEmpty())
} }
/** /**
@@ -63,15 +63,10 @@ object Utils {
} }
fun parseInt(str: String?, default: Int): Int { fun parseInt(str: String?, default: Int): Int {
str ?: return default return str?.toIntOrNull() ?: default
return try {
Integer.parseInt(str)
} catch (e: Exception) {
e.printStackTrace()
default
}
} }
/** /**
* get text from clipboard * get text from clipboard
*/ */
@@ -101,23 +96,19 @@ object Utils {
/** /**
* base64 decode * base64 decode
*/ */
fun decode(text: String): String { fun decode(text: String?): String {
tryDecodeBase64(text)?.let { return it } return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) }.orEmpty()
if (text.endsWith('=')) {
// try again for some loosely formatted base64
tryDecodeBase64(text.trimEnd('='))?.let { return it }
}
return ""
} }
fun tryDecodeBase64(text: String): String? {
fun tryDecodeBase64(text: String?): String? {
try { try {
return Base64.decode(text, Base64.NO_WRAP).toString(charset("UTF-8")) return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
} catch (e: Exception) { } catch (e: Exception) {
Log.i(ANG_PACKAGE, "Parse base64 standard failed $e") Log.i(ANG_PACKAGE, "Parse base64 standard failed $e")
} }
try { try {
return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(charset("UTF-8")) return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8)
} catch (e: Exception) { } catch (e: Exception) {
Log.i(ANG_PACKAGE, "Parse base64 url safe failed $e") Log.i(ANG_PACKAGE, "Parse base64 url safe failed $e")
} }
@@ -129,7 +120,7 @@ object Utils {
*/ */
fun encode(text: String): String { fun encode(text: String): String {
return try { return try {
Base64.encodeToString(text.toByteArray(charset("UTF-8")), Base64.NO_WRAP) Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
"" ""
@@ -149,7 +140,7 @@ object Utils {
} }
fun getVpnDnsServers(): List<String> { fun getVpnDnsServers(): List<String> {
val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS)?:AppConfig.DNS_VPN val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
return vpnDns.split(",").filter { isPureIpAddress(it) } return vpnDns.split(",").filter { isPureIpAddress(it) }
// allow empty, in that case dns will use system default // allow empty, in that case dns will use system default
} }
@@ -159,7 +150,7 @@ object Utils {
*/ */
fun getDomesticDnsServers(): List<String> { fun getDomesticDnsServers(): List<String> {
val domesticDns = settingsStorage?.decodeString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT val domesticDns = settingsStorage?.decodeString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
val ret = domesticDns.split(",").filter { isPureIpAddress(it) } val ret = domesticDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
if (ret.isEmpty()) { if (ret.isEmpty()) {
return listOf(AppConfig.DNS_DIRECT) return listOf(AppConfig.DNS_DIRECT)
} }
@@ -213,7 +204,8 @@ object Utils {
} }
fun isIpv4Address(value: String): Boolean { fun isIpv4Address(value: String): Boolean {
val regV4 = Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$") val regV4 =
Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$")
return regV4.matches(value) return regV4.matches(value)
} }
@@ -223,12 +215,13 @@ object Utils {
addr = addr.drop(1) addr = addr.drop(1)
addr = addr.dropLast(addr.count() - addr.lastIndexOf("]")) addr = addr.dropLast(addr.count() - addr.lastIndexOf("]"))
} }
val regV6 = Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$") val regV6 =
Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$")
return regV6.matches(addr) return regV6.matches(addr)
} }
private fun isCoreDNSAddress(s: String): Boolean { private fun isCoreDNSAddress(s: String): Boolean {
return s.startsWith("https") || s.startsWith("tcp") || s.startsWith("quic") return s.startsWith("https") || s.startsWith("tcp") || s.startsWith("quic") || s == "localhost"
} }
/** /**
@@ -236,7 +229,13 @@ object Utils {
*/ */
fun isValidUrl(value: String?): Boolean { fun isValidUrl(value: String?): Boolean {
try { try {
if (value != null && Patterns.WEB_URL.matcher(value).matches() || URLUtil.isValidUrl(value)) { if (value.isNullOrEmpty()) {
return false
}
if (Patterns.WEB_URL.matcher(value).matches()
|| Patterns.DOMAIN_NAME.matcher(value).matches()
|| URLUtil.isValidUrl(value)
) {
return true return true
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -282,7 +281,7 @@ object Utils {
fun urlDecode(url: String): String { fun urlDecode(url: String): String {
return try { return try {
URLDecoder.decode(url, "UTF-8") URLDecoder.decode(url, Charsets.UTF_8.toString())
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
url url
@@ -291,7 +290,7 @@ object Utils {
fun urlEncode(url: String): String { fun urlEncode(url: String): String {
return try { return try {
URLEncoder.encode(url, "UTF-8") URLEncoder.encode(url, Charsets.UTF_8.toString())
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
url url
@@ -302,7 +301,10 @@ object Utils {
/** /**
* readTextFromAssets * readTextFromAssets
*/ */
fun readTextFromAssets(context: Context, fileName: String): String { fun readTextFromAssets(context: Context?, fileName: String): String {
if (context == null) {
return ""
}
val content = context.assets.open(fileName).bufferedReader().use { val content = context.assets.open(fileName).bufferedReader().use {
it.readText() it.readText()
} }
@@ -326,7 +328,7 @@ object Utils {
} }
fun getDeviceIdForXUDPBaseKey(): String { fun getDeviceIdForXUDPBaseKey(): String {
val androidId = Settings.Secure.ANDROID_ID.toByteArray(charset("UTF-8")) val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE)) return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
} }
@@ -352,14 +354,27 @@ object Utils {
} }
@Throws(IOException::class) @Throws(IOException::class)
fun getUrlContentWithCustomUserAgent(urlStr: String?): String { fun getUrlContentWithCustomUserAgent(urlStr: String?, timeout: Int = 30000, httpPort: Int = 0): String {
val url = URL(urlStr) val url = URL(urlStr)
val conn = url.openConnection() val conn = if (httpPort == 0) {
url.openConnection()
} else {
url.openConnection(
Proxy(
Proxy.Type.HTTP,
InetSocketAddress("127.0.0.1", httpPort)
)
)
}
conn.connectTimeout = timeout
conn.readTimeout = timeout
conn.setRequestProperty("Connection", "close") conn.setRequestProperty("Connection", "close")
conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}") conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
url.userInfo?.let { url.userInfo?.let {
conn.setRequestProperty("Authorization", conn.setRequestProperty(
"Basic ${encode(urlDecode(it))}") "Authorization",
"Basic ${encode(urlDecode(it))}"
)
} }
conn.useCaches = false conn.useCaches = false
return conn.inputStream.use { return conn.inputStream.use {
@@ -368,10 +383,10 @@ object Utils {
} }
fun getDarkModeStatus(context: Context): Boolean { fun getDarkModeStatus(context: Context): Boolean {
val mode = context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK return context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK != UI_MODE_NIGHT_NO
return mode != UI_MODE_NIGHT_NO
} }
fun setNightMode(context: Context) { fun setNightMode(context: Context) {
when (settingsStorage?.decodeString(AppConfig.PREF_UI_MODE_NIGHT, "0")) { when (settingsStorage?.decodeString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) "0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
@@ -380,7 +395,10 @@ object Utils {
} }
} }
fun getIpv6Address(address: String): String { fun getIpv6Address(address: String?): String {
if (address == null) {
return ""
}
return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) { return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) {
String.format("[%s]", address) String.format("[%s]", address)
} else { } else {
@@ -388,17 +406,21 @@ object Utils {
} }
} }
fun getLocale(context: Context): Locale = fun getLocale(): Locale {
when (settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: "auto") { val lang = settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: "auto"
return when (lang) {
"auto" -> getSysLocale() "auto" -> getSysLocale()
"en" -> Locale("en") "en" -> Locale.ENGLISH
"zh-rCN" -> Locale("zh", "CN") "zh-rCN" -> Locale.CHINA
"zh-rTW" -> Locale("zh", "TW") "zh-rTW" -> Locale.TRADITIONAL_CHINESE
"vi" -> Locale("vi") "vi" -> Locale("vi")
"ru" -> Locale("ru") "ru" -> Locale("ru")
"fa" -> Locale("fa") "fa" -> Locale("fa")
"bn" -> Locale("bn")
else -> getSysLocale() else -> getSysLocale()
} }
}
private fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { private fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
LocaleList.getDefault()[0] LocaleList.getDefault()[0]
@@ -408,8 +430,8 @@ object Utils {
fun fixIllegalUrl(str: String): String { fun fixIllegalUrl(str: String): String {
return str return str
.replace(" ","%20") .replace(" ", "%20")
.replace("|","%7C") .replace("|", "%7C")
} }
fun removeWhiteSpace(str: String?): String? { fun removeWhiteSpace(str: String?): String? {
@@ -425,5 +447,14 @@ object Utils {
fun isTv(context: Context): Boolean = fun isTv(context: Context): Boolean =
context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
fun getDelayTestUrl(second: Boolean = false): String {
return if (second) {
AppConfig.DelayTestUrl2
} else {
settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DelayTestUrl
}
}
} }

View File

@@ -2,17 +2,21 @@ package com.v2ray.ang.util
import android.content.Context import android.content.Context
import android.text.TextUtils import android.text.TextUtils
import com.google.gson.* import android.util.Log
import com.google.gson.Gson
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM
import com.v2ray.ang.AppConfig.TAG_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.TAG_FRAGMENT 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_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6 import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ERoutingMode import com.v2ray.ang.dto.ERoutingMode
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK
import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP
@@ -49,6 +53,14 @@ object V2rayConfigUtil {
return Result(true, customConfig) return Result(true, customConfig)
} }
val outbound = config.getProxyOutbound() ?: return Result(false, "") val outbound = config.getProxyOutbound() ?: return Result(false, "")
val address = outbound.getServerAddress() ?: return Result(false, "")
if (!Utils.isIpAddress(address)) {
if (!Utils.isValidUrl(address)) {
Log.d(ANG_PACKAGE, "$address is an invalid ip or domain")
return Result(false, "")
}
}
val result = getV2rayNonCustomConfig(context, outbound, config.remarks) val result = getV2rayNonCustomConfig(context, outbound, config.remarks)
//Log.d(ANG_PACKAGE, result.content) //Log.d(ANG_PACKAGE, result.content)
return result return result
@@ -134,6 +146,8 @@ object V2rayConfigUtil {
settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true) settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true)
?: true ?: true
v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp
v2rayConfig.inbounds[0].sniffing?.routeOnly =
settingsStorage?.decodeBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
if (!sniffAllTlsAndHttp) { if (!sniffAllTlsAndHttp) {
v2rayConfig.inbounds[0].sniffing?.destOverride?.clear() v2rayConfig.inbounds[0].sniffing?.destOverride?.clear()
} }
@@ -158,13 +172,9 @@ object V2rayConfigUtil {
private fun fakedns(v2rayConfig: V2rayConfig) { private fun fakedns(v2rayConfig: V2rayConfig) {
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
|| settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true && settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
) { ) {
v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean()) v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean())
v2rayConfig.outbounds.filter { it.protocol == PROTOCOL_FREEDOM && it.tag == TAG_DIRECT }
.forEach {
it.settings?.domainStrategy = "UseIP"
}
} }
} }
@@ -173,65 +183,82 @@ object V2rayConfigUtil {
*/ */
private fun routing(v2rayConfig: V2rayConfig): Boolean { private fun routing(v2rayConfig: V2rayConfig): Boolean {
try { try {
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
routingUserRule(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
.orEmpty(), TAG_BLOCKED, v2rayConfig
)
if (routingMode == ERoutingMode.GLOBAL_DIRECT.value) {
routingUserRule(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
.orEmpty(), TAG_DIRECT, v2rayConfig
)
routingUserRule( routingUserRule(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT) settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
?: "", AppConfig.TAG_PROXY, v2rayConfig .orEmpty(), TAG_PROXY, v2rayConfig
)
} else {
routingUserRule(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
.orEmpty(), TAG_PROXY, v2rayConfig
) )
routingUserRule( routingUserRule(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT) settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
?: "", AppConfig.TAG_DIRECT, v2rayConfig .orEmpty(), TAG_DIRECT, v2rayConfig
)
routingUserRule(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
?: "", AppConfig.TAG_BLOCKED, v2rayConfig
) )
}
v2rayConfig.routing.domainStrategy = v2rayConfig.routing.domainStrategy =
settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
?: "IPIfNonMatch" ?: "IPIfNonMatch"
// v2rayConfig.routing.domainMatcher = "mph"
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
// Hardcode googleapis.cn // Hardcode googleapis.cn gstatic.com
val googleapisRoute = V2rayConfig.RoutingBean.RulesBean( val googleapisRoute = V2rayConfig.RoutingBean.RulesBean(
outboundTag = AppConfig.TAG_PROXY, outboundTag = TAG_PROXY,
domain = arrayListOf("domain:googleapis.cn") domain = arrayListOf("domain:googleapis.cn", "domain:gstatic.com")
) )
when (routingMode) { when (routingMode) {
ERoutingMode.BYPASS_LAN.value -> { ERoutingMode.BYPASS_LAN.value -> {
routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig) routingGeo("", "private", TAG_DIRECT, v2rayConfig)
} }
ERoutingMode.BYPASS_MAINLAND.value -> { ERoutingMode.BYPASS_MAINLAND.value -> {
routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig) routingGeo("", "cn", TAG_DIRECT, v2rayConfig)
routingGeo("domain", "geolocation-cn", AppConfig.TAG_DIRECT, v2rayConfig)
v2rayConfig.routing.rules.add(0, googleapisRoute) v2rayConfig.routing.rules.add(0, googleapisRoute)
} }
ERoutingMode.BYPASS_LAN_MAINLAND.value -> { ERoutingMode.BYPASS_LAN_MAINLAND.value -> {
routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig) routingGeo("", "private", TAG_DIRECT, v2rayConfig)
routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig) routingGeo("", "cn", TAG_DIRECT, v2rayConfig)
routingGeo("domain", "geolocation-cn", AppConfig.TAG_DIRECT, v2rayConfig)
v2rayConfig.routing.rules.add(0, googleapisRoute) v2rayConfig.routing.rules.add(0, googleapisRoute)
} }
ERoutingMode.GLOBAL_DIRECT.value -> { ERoutingMode.GLOBAL_DIRECT.value -> {
val globalDirect = V2rayConfig.RoutingBean.RulesBean( val globalDirect = V2rayConfig.RoutingBean.RulesBean(
outboundTag = AppConfig.TAG_DIRECT, outboundTag = TAG_DIRECT,
port = "0-65535"
) )
if (v2rayConfig.routing.domainStrategy != "IPIfNonMatch") {
globalDirect.port = "0-65535"
} else {
globalDirect.ip = arrayListOf("0.0.0.0/0", "::/0")
}
v2rayConfig.routing.rules.add(globalDirect) v2rayConfig.routing.rules.add(globalDirect)
} }
} }
if(routingMode != ERoutingMode.GLOBAL_DIRECT.value) { if (routingMode != ERoutingMode.GLOBAL_DIRECT.value) {
v2rayConfig.routing.rules.add( val globalProxy = V2rayConfig.RoutingBean.RulesBean(
V2rayConfig.RoutingBean.RulesBean( outboundTag = TAG_PROXY,
outboundTag = AppConfig.TAG_PROXY, )
port = "0-65535" if (v2rayConfig.routing.domainStrategy != "IPIfNonMatch") {
)) globalProxy.port = "0-65535"
} else {
globalProxy.ip = arrayListOf("0.0.0.0/0", "::/0")
}
v2rayConfig.routing.rules.add(globalProxy)
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -286,22 +313,18 @@ object V2rayConfigUtil {
rulesIP.ip = ArrayList() rulesIP.ip = ArrayList()
userRule.split(",").map { it.trim() }.forEach { userRule.split(",").map { it.trim() }.forEach {
if (Utils.isIpAddress(it) || it.startsWith("geoip:")) { if (it.startsWith("ext:") && it.contains("geoip")) {
rulesIP.ip?.add(it) rulesIP.ip?.add(it)
} else if (it.isNotEmpty()) } else if (Utils.isIpAddress(it) || it.startsWith("geoip:")) {
// if (Utils.isValidUrl(it) rulesIP.ip?.add(it)
// || it.startsWith("geosite:") } else if (it.isNotEmpty()) {
// || it.startsWith("regexp:")
// || it.startsWith("domain:")
// || it.startsWith("full:"))
{
rulesDomain.domain?.add(it) rulesDomain.domain?.add(it)
} }
} }
if (rulesDomain.domain?.size!! > 0) { if ((rulesDomain.domain?.size ?: 0) > 0) {
v2rayConfig.routing.rules.add(rulesDomain) v2rayConfig.routing.rules.add(rulesDomain)
} }
if (rulesIP.ip?.size!! > 0) { if ((rulesIP.ip?.size ?: 0) > 0) {
v2rayConfig.routing.rules.add(rulesIP) v2rayConfig.routing.rules.add(rulesIP)
} }
} }
@@ -310,7 +333,7 @@ object V2rayConfigUtil {
} }
} }
private fun userRule2Domian(userRule: String): ArrayList<String> { private fun userRule2Domain(userRule: String): ArrayList<String> {
val domain = ArrayList<String>() val domain = ArrayList<String>()
userRule.split(",").map { it.trim() }.forEach { userRule.split(",").map { it.trim() }.forEach {
if (it.startsWith("geosite:") || it.startsWith("domain:")) { if (it.startsWith("geosite:") || it.startsWith("domain:")) {
@@ -327,13 +350,13 @@ object V2rayConfigUtil {
try { try {
if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) { if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
val geositeCn = arrayListOf("geosite:cn") val geositeCn = arrayListOf("geosite:cn")
val proxyDomain = userRule2Domian( val proxyDomain = userRule2Domain(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT) settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
?: "" .orEmpty()
) )
val directDomain = userRule2Domian( val directDomain = userRule2Domain(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT) settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
?: "" .orEmpty()
) )
// fakedns with all domains to make it always top priority // fakedns with all domains to make it always top priority
v2rayConfig.dns.servers?.add( v2rayConfig.dns.servers?.add(
@@ -400,14 +423,15 @@ object V2rayConfigUtil {
private fun dns(v2rayConfig: V2rayConfig): Boolean { private fun dns(v2rayConfig: V2rayConfig): Boolean {
try { try {
val hosts = mutableMapOf<String, String>() val hosts = mutableMapOf<String, Any>()
val servers = ArrayList<Any>() val servers = ArrayList<Any>()
val remoteDns = Utils.getRemoteDnsServers()
val proxyDomain = userRule2Domian(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
?: ""
)
//remote Dns
val remoteDns = Utils.getRemoteDnsServers()
val proxyDomain = userRule2Domain(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
.orEmpty()
)
remoteDns.forEach { remoteDns.forEach {
servers.add(it) servers.add(it)
} }
@@ -423,27 +447,29 @@ object V2rayConfigUtil {
} }
// domestic DNS // domestic DNS
val directDomain = userRule2Domian( val domesticDns = Utils.getDomesticDnsServers()
val directDomain = userRule2Domain(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT) settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
?: "" .orEmpty()
) )
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
?: ERoutingMode.BYPASS_LAN_MAINLAND.value ?: ERoutingMode.BYPASS_LAN_MAINLAND.value
if (directDomain.size > 0 || routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) { val isCnRoutingMode =
val domesticDns = Utils.getDomesticDnsServers() (routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value)
val geositeCn = arrayListOf("geosite:cn","geosite:geolocation-cn")
val geoipCn = arrayListOf("geoip:cn") val geoipCn = arrayListOf("geoip:cn")
if (directDomain.size > 0) { if (directDomain.size > 0) {
servers.add( servers.add(
V2rayConfig.DnsBean.ServersBean( V2rayConfig.DnsBean.ServersBean(
domesticDns.first(), domesticDns.first(),
53, 53,
directDomain, directDomain,
geoipCn if (isCnRoutingMode) geoipCn else null
) )
) )
} }
if (routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) { if (isCnRoutingMode) {
val geositeCn = arrayListOf("geosite:cn")
servers.add( servers.add(
V2rayConfig.DnsBean.ServersBean( V2rayConfig.DnsBean.ServersBean(
domesticDns.first(), domesticDns.first(),
@@ -453,21 +479,22 @@ object V2rayConfigUtil {
) )
) )
} }
if (Utils.isPureIpAddress(domesticDns.first())) { if (Utils.isPureIpAddress(domesticDns.first())) {
v2rayConfig.routing.rules.add( v2rayConfig.routing.rules.add(
0, V2rayConfig.RoutingBean.RulesBean( 0, V2rayConfig.RoutingBean.RulesBean(
outboundTag = AppConfig.TAG_DIRECT, outboundTag = TAG_DIRECT,
port = "53", port = "53",
ip = arrayListOf(domesticDns.first()), ip = arrayListOf(domesticDns.first()),
domain = null domain = null
) )
) )
} }
}
val blkDomain = userRule2Domian( //block dns
val blkDomain = userRule2Domain(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED) settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
?: "" .orEmpty()
) )
if (blkDomain.size > 0) { if (blkDomain.size > 0) {
hosts.putAll(blkDomain.map { it to "127.0.0.1" }) hosts.putAll(blkDomain.map { it to "127.0.0.1" })
@@ -476,6 +503,12 @@ object V2rayConfigUtil {
// hardcode googleapi rule to fix play store problems // hardcode googleapi rule to fix play store problems
hosts["domain:googleapis.cn"] = "googleapis.com" hosts["domain:googleapis.cn"] = "googleapis.com"
// hardcode popular Android Private DNS rule to fix localhost DNS problem
hosts["dns.pub"] = arrayListOf("1.12.12.12", "120.53.53.53")
hosts["dns.alidns.com"] = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
hosts["one.one.one.one"] = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
hosts["dns.google"] = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
// DNS dns对象 // DNS dns对象
v2rayConfig.dns = V2rayConfig.DnsBean( v2rayConfig.dns = V2rayConfig.DnsBean(
servers = servers, servers = servers,
@@ -486,7 +519,7 @@ object V2rayConfigUtil {
if (Utils.isPureIpAddress(remoteDns.first())) { if (Utils.isPureIpAddress(remoteDns.first())) {
v2rayConfig.routing.rules.add( v2rayConfig.routing.rules.add(
0, V2rayConfig.RoutingBean.RulesBean( 0, V2rayConfig.RoutingBean.RulesBean(
outboundTag = AppConfig.TAG_PROXY, outboundTag = TAG_PROXY,
port = "53", port = "53",
ip = arrayListOf(remoteDns.first()), ip = arrayListOf(remoteDns.first()),
domain = null domain = null
@@ -559,7 +592,7 @@ object V2rayConfigUtil {
} else { } else {
path path
} }
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host!! outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host
} }
@@ -588,7 +621,8 @@ object V2rayConfigUtil {
mux = null mux = null
) )
var packets = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello" var packets =
settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
if (v2rayConfig.outbounds[0].streamSettings?.security == V2rayConfig.REALITY if (v2rayConfig.outbounds[0].streamSettings?.security == V2rayConfig.REALITY
&& packets == "tlshello" && packets == "tlshello"
) { ) {
@@ -606,7 +640,11 @@ object V2rayConfigUtil {
?: "50-100", ?: "50-100",
interval = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL) interval = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL)
?: "10-20" ?: "10-20"
) ),
noise = V2rayConfig.OutboundBean.OutSettingsBean.NoiseBean(
packet = "rand:100-200",
delay = "10-20",
),
) )
fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean( fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean(
sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean( sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(

View File

@@ -0,0 +1,159 @@
package com.v2ray.ang.util.fmt
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.Utils
import java.net.URI
object ShadowsocksFmt {
fun parseShadowsocks(str: String): ServerConfig? {
val config = ServerConfig.create(EConfigType.SHADOWSOCKS)
if (!tryResolveResolveSip002(str, config)) {
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
val indexSplit = result.indexOf("#")
if (indexSplit > 0) {
try {
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
result = if (indexS > 0) {
Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
Utils.decode(result)
}
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
val match = legacyPattern.matchEntire(result)
?: return null
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = match.groupValues[3].removeSurrounding("[", "]")
server.port = match.groupValues[4].toInt()
server.password = match.groupValues[2]
server.method = match.groupValues[1].lowercase()
}
}
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val remark = "#" + Utils.urlEncode(config.remarks)
val pw =
Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
val url = String.format(
"%s@%s:%s",
pw,
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + remark
}
private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {
try {
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
val method: String
val password: String
if (uri.userInfo.contains(":")) {
val arrUserInfo = uri.userInfo.split(":").map { it.trim() }
if (arrUserInfo.count() != 2) {
return false
}
method = arrUserInfo[0]
password = Utils.urlDecode(arrUserInfo[1])
} else {
val base64Decode = Utils.decode(uri.userInfo)
val arrUserInfo = base64Decode.split(":").map { it.trim() }
if (arrUserInfo.count() < 2) {
return false
}
method = arrUserInfo[0]
password = base64Decode.substringAfter(":")
}
val query = Utils.urlDecode(uri.query.orEmpty())
if (query != "") {
val queryPairs = HashMap<String, String>()
val pairs = query.split(";")
Log.d(AppConfig.ANG_PACKAGE, pairs.toString())
for (pair in pairs) {
val idx = pair.indexOf("=")
if (idx == -1) {
queryPairs[Utils.urlDecode(pair)] = ""
} else {
queryPairs[Utils.urlDecode(pair.substring(0, idx))] =
Utils.urlDecode(pair.substring(idx + 1))
}
}
Log.d(AppConfig.ANG_PACKAGE, queryPairs.toString())
var sni: String? = ""
if (queryPairs["plugin"] == "obfs-local" && queryPairs["obfs"] == "http") {
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
"tcp",
"http",
queryPairs["obfs-host"],
queryPairs["path"],
null,
null,
null,
null,
null,
null
)
} else if (queryPairs["plugin"] == "v2ray-plugin") {
var network = "ws"
if (queryPairs["mode"] == "quic") {
network = "quic"
}
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
network,
null,
queryPairs["host"],
queryPairs["path"],
null,
null,
null,
null,
null,
null
)
}
if ("tls" in queryPairs) {
config.outboundBean?.streamSettings?.populateTlsSettings(
"tls", false, sni.orEmpty(), null, null, null, null, null
)
}
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = uri.idnHost
server.port = uri.port
server.password = password
server.method = method
}
return true
} catch (e: Exception) {
Log.d(AppConfig.ANG_PACKAGE, e.toString())
return false
}
}
}

View File

@@ -0,0 +1,69 @@
package com.v2ray.ang.util.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.util.Utils
object SocksFmt {
fun parseSocks(str: String): ServerConfig? {
val config = ServerConfig.create(EConfigType.SOCKS)
var result = str.replace(EConfigType.SOCKS.protocolScheme, "")
val indexSplit = result.indexOf("#")
if (indexSplit > 0) {
try {
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
if (indexS > 0) {
result = Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
result = Utils.decode(result)
}
val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex()
val match =
legacyPattern.matchEntire(result) ?: return null
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = match.groupValues[3].removeSurrounding("[", "]")
server.port = match.groupValues[4].toInt()
val socksUsersBean =
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = match.groupValues[1]
socksUsersBean.pass = match.groupValues[2]
server.users = listOf(socksUsersBean)
}
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val remark = "#" + Utils.urlEncode(config.remarks)
val pw =
if (outbound.settings?.servers?.get(0)?.users?.get(0)?.user != null)
"${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}"
else
":"
val url = String.format(
"%s@%s:%s",
Utils.encode(pw),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + remark
}
}

View File

@@ -0,0 +1,180 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import java.net.URI
object TrojanFmt {
private val settingsStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_SETTING,
MMKV.MULTI_PROCESS_MODE
)
}
fun parseTrojan(str: String): ServerConfig {
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val config = ServerConfig.create(EConfigType.TROJAN)
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
var flow = ""
var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint
if (uri.rawQuery.isNullOrEmpty()) {
config.outboundBean?.streamSettings?.populateTlsSettings(
V2rayConfig.TLS,
allowInsecure,
"",
fingerprint,
null,
null,
null,
null
)
} else {
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val sni = config.outboundBean?.streamSettings?.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"]
)
fingerprint = queryParam["fp"].orEmpty()
allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
config.outboundBean?.streamSettings?.populateTlsSettings(
queryParam["security"] ?: V2rayConfig.TLS,
allowInsecure,
queryParam["sni"] ?: sni.orEmpty(),
fingerprint,
queryParam["alpn"],
null,
null,
null
)
flow = queryParam["flow"].orEmpty()
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = uri.idnHost
server.port = uri.port
server.password = uri.userInfo
server.flow = flow
}
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
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
}
}

View File

@@ -0,0 +1,171 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import java.net.URI
object VlessFmt {
private val settingsStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_SETTING,
MMKV.MULTI_PROCESS_MODE
)
}
fun parseVless(str: String): ServerConfig? {
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val config = ServerConfig.create(EConfigType.VLESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val streamSetting = config.outboundBean?.streamSettings ?: return null
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = uri.idnHost
vnext.port = uri.port
vnext.users[0].id = uri.userInfo
vnext.users[0].encryption = queryParam["encryption"] ?: "none"
vnext.users[0].flow = queryParam["flow"].orEmpty()
}
val sni = streamSetting.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"]
)
allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
streamSetting.populateTlsSettings(
queryParam["security"].orEmpty(),
allowInsecure,
queryParam["sni"] ?: sni,
queryParam["fp"].orEmpty(),
queryParam["alpn"],
queryParam["pbk"].orEmpty(),
queryParam["sid"].orEmpty(),
queryParam["spx"].orEmpty()
)
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let {
if (!TextUtils.isEmpty(it)) {
dicQuery["flow"] = it
}
}
dicQuery["encryption"] =
if (outbound.getSecurityEncryption().isNullOrEmpty()) "none"
else outbound.getSecurityEncryption().orEmpty()
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
(streamSetting.tlsSettings
?: streamSetting.realitySettings)?.let { tlsSetting ->
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
dicQuery["sni"] = tlsSetting.serverName
}
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
dicQuery["alpn"] =
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
dicQuery["fp"] = tlsSetting.fingerprint.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
dicQuery["pbk"] = tlsSetting.publicKey.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
dicQuery["sid"] = tlsSetting.shortId.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX.orEmpty())
}
}
dicQuery["type"] =
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
outbound.getTransportSettingDetails()?.let { transportDetails ->
when (streamSetting.network) {
"tcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
}
"kcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
}
}
"ws", "httpupgrade", "splithttp" -> {
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"http", "h2" -> {
dicQuery["type"] = "http"
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"quic" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
}
"grpc" -> {
dicQuery["mode"] = transportDetails[0]
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
}
}
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
outbound.getPassword(),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + query + remark
}
}

View File

@@ -0,0 +1,160 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
import android.util.Log
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.VmessQRCode
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import java.net.URI
object VmessFmt {
private val settingsStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE
)
}
fun parseVmess(str: String): ServerConfig? {
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
return parseVmessStd(str)
}
val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val config = ServerConfig.create(EConfigType.VMESS)
val streamSetting = config.outboundBean?.streamSettings ?: return null
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
result = Utils.decode(result)
if (TextUtils.isEmpty(result)) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
return null
}
val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java)
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
if (TextUtils.isEmpty(vmessQRCode.add)
|| TextUtils.isEmpty(vmessQRCode.port)
|| TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net)
) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
return null
}
config.remarks = vmessQRCode.ps
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = vmessQRCode.add
vnext.port = Utils.parseInt(vmessQRCode.port)
vnext.users[0].id = vmessQRCode.id
vnext.users[0].security =
if (TextUtils.isEmpty(vmessQRCode.scy)) V2rayConfig.DEFAULT_SECURITY else vmessQRCode.scy
vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid)
}
val sni = streamSetting.populateTransportSettings(
vmessQRCode.net,
vmessQRCode.type,
vmessQRCode.host,
vmessQRCode.path,
vmessQRCode.path,
vmessQRCode.host,
vmessQRCode.path,
vmessQRCode.type,
vmessQRCode.path,
vmessQRCode.host
)
val fingerprint = vmessQRCode.fp
streamSetting.populateTlsSettings(
vmessQRCode.tls,
allowInsecure,
if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni,
fingerprint,
vmessQRCode.alpn,
null,
null,
null
)
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
val vmessQRCode = VmessQRCode()
vmessQRCode.v = "2"
vmessQRCode.ps = config.remarks
vmessQRCode.add = outbound.getServerAddress().orEmpty()
vmessQRCode.port = outbound.getServerPort().toString()
vmessQRCode.id = outbound.getPassword().orEmpty()
vmessQRCode.aid = outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()
vmessQRCode.scy = outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString()
vmessQRCode.net = streamSetting.network
vmessQRCode.tls = streamSetting.security
vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty()
vmessQRCode.alpn =
Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString()).orEmpty()
vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty()
outbound.getTransportSettingDetails()?.let { transportDetails ->
vmessQRCode.type = transportDetails[0]
vmessQRCode.host = transportDetails[1]
vmessQRCode.path = transportDetails[2]
}
val json = Gson().toJson(vmessQRCode)
return Utils.encode(json)
}
fun parseVmessStd(str: String): ServerConfig? {
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val config = ServerConfig.create(EConfigType.VMESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val streamSetting = config.outboundBean?.streamSettings ?: return null
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = uri.idnHost
vnext.port = uri.port
vnext.users[0].id = uri.userInfo
vnext.users[0].security = V2rayConfig.DEFAULT_SECURITY
vnext.users[0].alterId = 0
}
val sni = streamSetting.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"]
)
allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
streamSetting.populateTlsSettings(
queryParam["security"].orEmpty(),
allowInsecure,
queryParam["sni"] ?: sni,
queryParam["fp"].orEmpty(),
queryParam["alpn"],
null,
null,
null
)
return config
}
}

View File

@@ -0,0 +1,107 @@
package com.v2ray.ang.util.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.util.Utils
import java.net.URI
object WireguardFmt {
fun parseWireguard(str: String): ServerConfig? {
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery != null) {
val config = ServerConfig.create(EConfigType.WIREGUARD)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
config.outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = uri.userInfo
wireguard.address =
(queryParam["address"]
?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace()
.split(",")
wireguard.peers?.get(0)?.publicKey = queryParam["publickey"].orEmpty()
wireguard.peers?.get(0)?.endpoint =
Utils.getIpv6Address(uri.idnHost) + ":${uri.port}"
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
wireguard.reserved =
(queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",")
.map { it.toInt() }
}
return config
} else {
return null
}
}
fun parseWireguardConfFile(str: String): ServerConfig? {
val config = ServerConfig.create(EConfigType.WIREGUARD)
val queryParam: MutableMap<String, String> = mutableMapOf()
var currentSection: String? = null
str.lines().forEach { line ->
val trimmedLine = line.trim()
when {
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
trimmedLine.isBlank() || trimmedLine.startsWith("#") -> Unit // Skip blank lines or comments
currentSection != null -> {
val (key, value) = trimmedLine.split("=").map { it.trim() }
queryParam[key.lowercase()] = value // Store the key in lowercase for case-insensitivity
}
}
}
config.outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = queryParam["privatekey"].orEmpty()
wireguard.address = (queryParam["address"] ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace().split(",")
wireguard.peers?.getOrNull(0)?.publicKey = queryParam["publickey"].orEmpty()
wireguard.peers?.getOrNull(0)?.endpoint = queryParam["endpoint"].orEmpty()
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
wireguard.reserved = (queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",").map { it.toInt() }
}
return config
}
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())
if (outbound.settings?.reserved != null) {
dicQuery["reserved"] = Utils.urlEncode(
Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString())
.toString()
)
}
dicQuery["address"] = Utils.urlEncode(
Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString())
.toString()
)
if (outbound.settings?.mtu != null) {
dicQuery["mtu"] = outbound.settings?.mtu.toString()
}
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
}
}

View File

@@ -1,58 +1,56 @@
package com.v2ray.ang.viewmodel package com.v2ray.ang.viewmodel
import android.app.Application import android.app.Application
import android.content.* import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.AssetManager
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.gson.Gson import com.google.gson.Gson
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AngApplication import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.DialogConfigFilterBinding import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.* import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.* import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.KEY_ANG_CONFIGS import com.v2ray.ang.util.MmkvManager.KEY_ANG_CONFIGS
import kotlinx.coroutines.* import com.v2ray.ang.util.MmkvManager.subStorage
import java.util.* import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.V2rayConfigUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.util.Collections
class MainViewModel(application: Application) : AndroidViewModel(application) { class MainViewModel(application: Application) : AndroidViewModel(application) {
private val mainStorage by lazy { private var serverList = MmkvManager.decodeServerList()
MMKV.mmkvWithID( var subscriptionId: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
MmkvManager.ID_MAIN,
MMKV.MULTI_PROCESS_MODE
)
}
private val serverRawStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_SERVER_RAW,
MMKV.MULTI_PROCESS_MODE
)
}
private val settingsStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_SETTING,
MMKV.MULTI_PROCESS_MODE
)
}
var serverList = MmkvManager.decodeServerList() //var keywordFilter: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
var subscriptionId: String = settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "")!! var keywordFilter = ""
var keywordFilter: String = settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")!!
private set
val serversCache = mutableListOf<ServersCache>() val serversCache = mutableListOf<ServersCache>()
val isRunning by lazy { MutableLiveData<Boolean>() } val isRunning by lazy { MutableLiveData<Boolean>() }
val updateListAction by lazy { MutableLiveData<Int>() } val updateListAction by lazy { MutableLiveData<Int>() }
val updateTestResultAction by lazy { MutableLiveData<String>() } val updateTestResultAction by lazy { MutableLiveData<String>() }
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) } private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
fun startListenBroadcast() { fun startListenBroadcast() {
@@ -95,49 +93,108 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
} }
fun appendCustomConfigServer(server: String) { fun appendCustomConfigServer(server: String): Boolean {
if (server.contains("inbounds")
&& server.contains("outbounds")
&& server.contains("routing")
) {
try {
val config = ServerConfig.create(EConfigType.CUSTOM) val config = ServerConfig.create(EConfigType.CUSTOM)
config.subscriptionId = subscriptionId config.subscriptionId = subscriptionId
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java) config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString() config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
val key = MmkvManager.encodeServerConfig("", config) val key = MmkvManager.encodeServerConfig("", config)
serverRawStorage?.encode(key, server) MmkvManager.serverRawStorage?.encode(key, server)
serverList.add(0, key) serverList.add(0, key)
serversCache.add(0, ServersCache(key, config)) val profile = ProfileItem(
configType = config.configType,
subscriptionId = config.subscriptionId,
remarks = config.remarks,
server = config.getProxyOutbound()?.getServerAddress(),
serverPort = config.getProxyOutbound()?.getServerPort(),
)
serversCache.add(0, ServersCache(key, profile))
return true
} catch (e: Exception) {
e.printStackTrace()
}
}
return false
} }
fun swapServer(fromPosition: Int, toPosition: Int) { fun swapServer(fromPosition: Int, toPosition: Int) {
Collections.swap(serverList, fromPosition, toPosition) Collections.swap(serverList, fromPosition, toPosition)
Collections.swap(serversCache, fromPosition, toPosition) Collections.swap(serversCache, fromPosition, toPosition)
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList)) MmkvManager.mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
} }
@Synchronized @Synchronized
fun updateCache() { fun updateCache() {
serversCache.clear() serversCache.clear()
for (guid in serverList) { for (guid in serverList) {
var profile = MmkvManager.decodeProfileConfig(guid)
if (profile == null) {
val config = MmkvManager.decodeServerConfig(guid) ?: continue val config = MmkvManager.decodeServerConfig(guid) ?: continue
if (subscriptionId.isNotEmpty() && subscriptionId != config.subscriptionId) { profile = ProfileItem(
configType = config.configType,
subscriptionId = config.subscriptionId,
remarks = config.remarks,
server = config.getProxyOutbound()?.getServerAddress(),
serverPort = config.getProxyOutbound()?.getServerPort(),
)
MmkvManager.encodeServerConfig(guid, config)
}
if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) {
continue continue
} }
if (keywordFilter.isEmpty() || config.remarks.contains(keywordFilter)) { if (keywordFilter.isEmpty() || profile.remarks.contains(keywordFilter)) {
serversCache.add(ServersCache(guid, config)) serversCache.add(ServersCache(guid, profile))
} }
} }
} }
fun updateConfigViaSubAll(): Int {
if (subscriptionId.isNullOrEmpty()) {
return AngConfigManager.updateConfigViaSubAll()
} else {
val json = subStorage?.decodeString(subscriptionId)
if (!json.isNullOrBlank()) {
return updateConfigViaSub(Pair(subscriptionId, Gson().fromJson(json, SubscriptionItem::class.java)))
} else {
return 0
}
}
}
fun exportAllServer(): Int {
val serverListCopy =
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
serverList
} else {
serversCache.map { it.guid }.toList()
}
val ret = AngConfigManager.shareNonCustomConfigsToClipboard(
getApplication<AngApplication>(),
serverListCopy
)
return ret
}
fun testAllTcping() { fun testAllTcping() {
tcpingTestScope.coroutineContext[Job]?.cancelChildren() tcpingTestScope.coroutineContext[Job]?.cancelChildren()
SpeedtestUtil.closeAllTcpSockets() SpeedtestUtil.closeAllTcpSockets()
MmkvManager.clearAllTestDelayResults() 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) getApplication<AngApplication>().toast(R.string.connection_test_testing)
for (item in serversCache) { for (item in serversCache) {
item.config.getProxyOutbound()?.let { outbound -> item.profile.let { outbound ->
val serverAddress = outbound.getServerAddress() val serverAddress = outbound.server
val serverPort = outbound.getServerPort() val serverPort = outbound.serverPort
if (serverAddress != null && serverPort != null) { if (serverAddress != null && serverPort != null) {
tcpingTestScope.launch { tcpingTestScope.launch {
val testResult = SpeedtestUtil.tcping(serverAddress, serverPort) val testResult = SpeedtestUtil.tcping(serverAddress, serverPort)
@@ -153,7 +210,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun testAllRealPing() { fun testAllRealPing() {
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "") MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "")
MmkvManager.clearAllTestDelayResults() MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
updateListAction.value = -1 // update all updateListAction.value = -1 // update all
val serversCopy = serversCache.toList() // Create a copy of the list val serversCopy = serversCache.toList() // Create a copy of the list
@@ -177,60 +234,30 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "") MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "")
} }
fun filterConfig(context: Context) { fun subscriptionIdChanged(id: String) {
val subscriptions = MmkvManager.decodeSubscriptions() if (subscriptionId != id) {
val listId = subscriptions.map { it.first }.toList().toMutableList() subscriptionId = id
val listRemarks = subscriptions.map { it.second.remarks }.toList().toMutableList() MmkvManager.settingsStorage.encode(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId)
listRemarks += context.getString(R.string.filter_config_all)
val checkedItem = if (subscriptionId.isNotEmpty()) {
listId.indexOf(subscriptionId)
} else {
listRemarks.count() - 1
}
val ivBinding = DialogConfigFilterBinding.inflate(LayoutInflater.from(context))
ivBinding.spSubscriptionId.adapter = ArrayAdapter<String>(
context,
android.R.layout.simple_spinner_dropdown_item,
listRemarks
)
ivBinding.spSubscriptionId.setSelection(checkedItem)
ivBinding.etKeyword.text = Utils.getEditable(keywordFilter)
val builder = AlertDialog.Builder(context).setView(ivBinding.root)
builder.setTitle(R.string.title_filter_config)
builder.setPositiveButton(R.string.tasker_setting_confirm) { dialogInterface: DialogInterface?, _: Int ->
try {
val position = ivBinding.spSubscriptionId.selectedItemPosition
subscriptionId = if (listRemarks.count() - 1 == position) {
""
} else {
subscriptions[position].first
}
keywordFilter = ivBinding.etKeyword.text.toString()
settingsStorage?.encode(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId)
settingsStorage?.encode(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
reloadServerList() reloadServerList()
}
}
dialogInterface?.dismiss() fun getSubscriptions(context: Context): Pair<MutableList<String>?, MutableList<String>?> {
} catch (e: Exception) { val subscriptions = MmkvManager.decodeSubscriptions()
e.printStackTrace() if (subscriptionId.isNotEmpty()
&& !subscriptions.map { it.first }.contains(subscriptionId)
) {
subscriptionIdChanged("")
} }
if (subscriptions.isEmpty()) {
return null to null
} }
builder.show() val listId = subscriptions.map { it.first }.toMutableList()
// AlertDialog.Builder(context) listId.add(0, "")
// .setSingleChoiceItems(listRemarks.toTypedArray(), checkedItem) { dialog, i -> val listRemarks = subscriptions.map { it.second.remarks }.toMutableList()
// try { listRemarks.add(0, context.getString(R.string.filter_config_all))
// subscriptionId = if (listRemarks.count() - 1 == i) {
// "" return listId to listRemarks
// } else {
// subscriptions[i].first
// }
// reloadServerList()
// dialog.dismiss()
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }.show()
} }
fun getPosition(guid: String): Int { fun getPosition(guid: String): Int {
@@ -241,15 +268,21 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return -1 return -1
} }
fun removeDuplicateServer() { fun removeDuplicateServer(): Int {
val serversCacheCopy = mutableListOf<Pair<String, ServerConfig>>()
for (it in serversCache) {
val config = MmkvManager.decodeServerConfig(it.guid) ?: continue
serversCacheCopy.add(Pair(it.guid, config))
}
val deleteServer = mutableListOf<String>() val deleteServer = mutableListOf<String>()
serversCache.forEachIndexed { index, it -> serversCacheCopy.forEachIndexed { index, it ->
val outbound = it.config.getProxyOutbound() val outbound = it.second.getProxyOutbound()
serversCache.forEachIndexed { index2, it2 -> serversCacheCopy.forEachIndexed { index2, it2 ->
if (index2 > index) { if (index2 > index) {
val outbound2 = it2.config.getProxyOutbound() val outbound2 = it2.second.getProxyOutbound()
if (outbound == outbound2 && !deleteServer.contains(it2.guid)) { if (outbound == outbound2 && !deleteServer.contains(it2.first)) {
deleteServer.add(it2.guid) deleteServer.add(it2.first)
} }
} }
} }
@@ -257,13 +290,70 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
for (it in deleteServer) { for (it in deleteServer) {
MmkvManager.removeServer(it) MmkvManager.removeServer(it)
} }
return deleteServer.count()
}
fun removeAllServer() {
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
MmkvManager.removeAllServer()
} else {
val serversCopy = serversCache.toList()
for (item in serversCopy) {
MmkvManager.removeServer(item.guid)
}
}
}
fun removeInvalidServer() {
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
MmkvManager.removeInvalidServer("")
} else {
val serversCopy = serversCache.toList()
for (item in serversCopy) {
MmkvManager.removeInvalidServer(item.guid)
}
}
}
fun sortByTestResults() {
MmkvManager.sortByTestResults()
}
fun copyAssets(assets: AssetManager) {
val extFolder = Utils.userAssetPath(getApplication<AngApplication>())
viewModelScope.launch(Dispatchers.Default) {
try {
val geo = arrayOf("geosite.dat", "geoip.dat")
assets.list("")
?.filter { geo.contains(it) }
?.filter { !File(extFolder, it).exists() }
?.forEach {
val target = File(extFolder, it)
assets.open(it).use { input ->
FileOutputStream(target).use { output ->
input.copyTo(output)
}
}
Log.i(
ANG_PACKAGE,
"Copied from apk assets folder to ${target.absolutePath}"
)
}
} catch (e: Exception) {
Log.e(ANG_PACKAGE, "asset copy failed", e)
}
}
}
fun filterConfig(keyword: String) {
if (keyword == keywordFilter) {
return
}
keywordFilter = keyword
MmkvManager.settingsStorage.encode(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
reloadServerList() reloadServerList()
getApplication<AngApplication>().toast(
getApplication<AngApplication>().getString(
R.string.title_del_duplicate_config_count,
deleteServer.count()
)
)
} }
private val mMsgReceiver = object : BroadcastReceiver() { private val mMsgReceiver = object : BroadcastReceiver() {
@@ -296,7 +386,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> { AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
val resultPair = intent.getSerializableExtra("content") as Pair<String, Long> 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>
}
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second) MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
updateListAction.value = getPosition(resultPair.first) updateListAction.value = getPosition(resultPair.first)
} }

View File

@@ -39,6 +39,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
AppConfig.PREF_VPN_DNS, AppConfig.PREF_VPN_DNS,
AppConfig.PREF_REMOTE_DNS, AppConfig.PREF_REMOTE_DNS,
AppConfig.PREF_DOMESTIC_DNS, AppConfig.PREF_DOMESTIC_DNS,
AppConfig.PREF_DELAY_TEST_URL,
AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PREF_LOCAL_DNS_PORT,
AppConfig.PREF_SOCKS_PORT, AppConfig.PREF_SOCKS_PORT,
AppConfig.PREF_HTTP_PORT, AppConfig.PREF_HTTP_PORT,
@@ -59,6 +60,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
settingsStorage?.encode(key, sharedPreferences.getString(key, "")) settingsStorage?.encode(key, sharedPreferences.getString(key, ""))
} }
AppConfig.PREF_ROUTE_ONLY_ENABLED,
AppConfig.PREF_SPEED_ENABLED, AppConfig.PREF_SPEED_ENABLED,
AppConfig.PREF_PROXY_SHARING, AppConfig.PREF_PROXY_SHARING,
AppConfig.PREF_LOCAL_DNS_ENABLED, AppConfig.PREF_LOCAL_DNS_ENABLED,

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"
android:fromAlpha="0.0"
android:interpolator="@android:interpolator/decelerate_quad"
android:toAlpha="1.0" />

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"
android:fromAlpha="1.0"
android:interpolator="@android:interpolator/accelerate_quad"
android:toAlpha="0.0" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 B

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/> android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector> </vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M11.8,10.9c-2.27,-0.59 -3,-1.2 -3,-2.15 0,-1.09 1.01,-1.85 2.7,-1.85 1.78,0 2.44,0.85 2.5,2.1h2.21c-0.07,-1.72 -1.12,-3.3 -3.21,-3.81V3h-3v2.16c-1.94,0.42 -3.5,1.68 -3.5,3.61 0,2.31 1.91,3.46 4.7,4.13 2.5,0.6 3,1.48 3,2.41 0,0.69 -0.49,1.79 -2.7,1.79 -2.06,0 -2.87,-0.92 -2.98,-2.1h-2.2c0.12,2.19 1.76,3.42 3.68,3.83V21h3v-2.15c1.95,-0.37 3.5,-1.5 3.5,-3.55 0,-2.84 -2.43,-3.81 -4.7,-4.4z"/>
</vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/> android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/> android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z"/> android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path
android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z" android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z"
android:fillColor="#FFFFFFFF"/> android:fillColor="#FFFFFFFF" />
</vector> </vector>

View File

@@ -5,11 +5,11 @@
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path
android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z" android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z"
android:fillColor="#FFFFFFFF"/> android:fillColor="#FFFFFFFF" />
<path <path
android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z" android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z"
android:fillColor="#FFFFFFFF"/> android:fillColor="#FFFFFFFF" />
<path <path
android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z" android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z"
android:fillColor="#FFFFFFFF"/> android:fillColor="#FFFFFFFF" />
</vector> </vector>

View File

@@ -1,5 +1,10 @@
<vector android:height="24dp" android:tint="#FFFFFF" <vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24" android:tint="#FFFFFF"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:viewportHeight="24"
<path android:fillColor="@android:color/white" android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z"/> android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z" />
</vector> </vector>

View File

@@ -5,8 +5,8 @@
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path
android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z" android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z"
android:fillColor="#FFFFFFFF"/> android:fillColor="#FFFFFFFF" />
<path <path
android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z" android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z"
android:fillColor="#FFFFFFFF"/> android:fillColor="#FFFFFFFF" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/> android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/> android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="#FF000000"
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/> android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="#FF000000"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector> </vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M11.8,10.9c-2.27,-0.59 -3,-1.2 -3,-2.15 0,-1.09 1.01,-1.85 2.7,-1.85 1.78,0 2.44,0.85 2.5,2.1h2.21c-0.07,-1.72 -1.12,-3.3 -3.21,-3.81V3h-3v2.16c-1.94,0.42 -3.5,1.68 -3.5,3.61 0,2.31 1.91,3.46 4.7,4.13 2.5,0.6 3,1.48 3,2.41 0,0.69 -0.49,1.79 -2.7,1.79 -2.06,0 -2.87,-0.92 -2.98,-2.1h-2.2c0.12,2.19 1.76,3.42 3.68,3.83V21h3v-2.15c1.95,-0.37 3.5,-1.5 3.5,-3.55 0,-2.84 -2.43,-3.81 -4.7,-4.4z"/>
</vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="#FF000000"
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/> android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="#FF000000"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/> android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="#FF000000"
android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z"/> android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="#FF000000"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path
android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z" android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z"
android:fillColor="#FF000000"/> android:fillColor="#FF000000" />
</vector> </vector>

View File

@@ -5,11 +5,11 @@
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path
android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z" android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z"
android:fillColor="#FF000000"/> android:fillColor="#FF000000" />
<path <path
android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z" android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z"
android:fillColor="#FF000000"/> android:fillColor="#FF000000" />
<path <path
android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z" android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z"
android:fillColor="#FF000000"/> android:fillColor="#FF000000" />
</vector> </vector>

View File

@@ -1,5 +1,10 @@
<vector android:height="24dp" android:tint="#000000" <vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24" android:tint="#000000"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:viewportHeight="24"
<path android:fillColor="@android:color/white" android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z"/> android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z" />
</vector> </vector>

View File

@@ -5,8 +5,8 @@
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path
android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z" android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z"
android:fillColor="#FF000000"/> android:fillColor="#FF000000" />
<path <path
android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z" android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z"
android:fillColor="#FF000000"/> android:fillColor="#FF000000" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="#FF000000"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/> android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
</vector> </vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="#FF000000"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/> android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
</vector> </vector>

View File

@@ -1,4 +1,9 @@
<vector android:height="24dp" android:viewportHeight="1024" <vector android:height="24dp"
android:viewportWidth="1024" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:viewportHeight="1024"
<path android:fillColor="#FF000000" android:pathData="M469.2,802.1l-81.7,-24.6L554.8,221.9l81.7,24.6L469.2,802.1zM362.7,654.5l-124.7,-141.7 124.8,-143.5 -64.4,-56 -173.8,199.8 174,197.7 64.1,-56.3zM899.4,513.1l-173.8,-199.8 -64.4,56 124.8,143.5 -124.7,141.7 64.1,56.4 174,-197.7z"/> android:viewportWidth="1024"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#FF000000"
android:pathData="M469.2,802.1l-81.7,-24.6L554.8,221.9l81.7,24.6L469.2,802.1zM362.7,654.5l-124.7,-141.7 124.8,-143.5 -64.4,-56 -173.8,199.8 174,197.7 64.1,-56.3zM899.4,513.1l-173.8,-199.8 -64.4,56 124.8,143.5 -124.7,141.7 64.1,56.4 174,-197.7z" />
</vector> </vector>

View File

@@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
<LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
@@ -17,7 +20,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:foreground="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:gravity="center|start" android:gravity="center|start"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="16dp"> android:padding="16dp">
@@ -51,13 +54,37 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/layout_share"
android:layout_width="match_parent"
android:layout_height="@dimen/server_height"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:layout_width="@dimen/png_height"
android:layout_height="@dimen/png_height"
app:srcCompat="@drawable/ic_share_24dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:text="@string/title_configuration_share"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/layout_restore" android:id="@+id/layout_restore"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/server_height" android:layout_height="@dimen/server_height"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:foreground="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:gravity="center|start" android:gravity="center|start"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="16dp"> android:padding="16dp">
@@ -89,7 +116,7 @@
android:layout_height="@dimen/server_height" android:layout_height="@dimen/server_height"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:foreground="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:gravity="center|start" android:gravity="center|start"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="16dp"> android:padding="16dp">
@@ -113,7 +140,7 @@
android:layout_height="@dimen/server_height" android:layout_height="@dimen/server_height"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:foreground="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:gravity="center|start" android:gravity="center|start"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="16dp"> android:padding="16dp">
@@ -138,7 +165,7 @@
android:layout_height="@dimen/server_height" android:layout_height="@dimen/server_height"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:foreground="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:gravity="center|start" android:gravity="center|start"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="16dp"> android:padding="16dp">
@@ -162,7 +189,7 @@
android:layout_height="@dimen/server_height" android:layout_height="@dimen/server_height"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:foreground="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:gravity="center|start" android:gravity="center|start"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="16dp"> android:padding="16dp">
@@ -197,6 +224,7 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</ScrollView>

View File

@@ -37,6 +37,22 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/pb_waiting"
app:indicatorColor="@color/color_fab_active"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="invisible" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_group"
app:tabMode="scrollable"
app:tabTextAppearance="@style/TabLayoutTextStyle"
app:tabIndicatorFullWidth="false"
android:layout_height="wrap_content"
android:layout_width="match_parent" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -63,12 +79,11 @@
android:minLines="1" android:minLines="1"
android:paddingStart="16dp" android:paddingStart="16dp"
android:text="@string/connection_test_pending" android:text="@string/connection_test_pending"
android:textAppearance="@style/TextAppearance.AppCompat.Small"/> android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<FrameLayout <FrameLayout
android:id="@+id/fabProgressCircle" android:id="@+id/fabProgressCircle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -105,7 +120,7 @@
android:layout_gravity="start" android:layout_gravity="start"
app:headerLayout="@layout/nav_header" app:headerLayout="@layout/nav_header"
app:itemIconTint="@color/colorAccent" app:itemIconTint="@color/colorAccent"
app:menu="@menu/menu_drawer" > app:menu="@menu/menu_drawer">
</com.google.android.material.navigation.NavigationView> </com.google.android.material.navigation.NavigationView>

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"></RelativeLayout>
</RelativeLayout>

View File

@@ -1,7 +1,6 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout

View File

@@ -173,6 +173,7 @@
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/tv_request_host"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/server_lab_request_host" /> android:text="@string/server_lab_request_host" />
@@ -192,6 +193,7 @@
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/tv_path"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/server_lab_path" /> android:text="@string/server_lab_path" />

View File

@@ -154,6 +154,7 @@
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/tv_request_host"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/server_lab_request_host" /> android:text="@string/server_lab_request_host" />
@@ -173,6 +174,7 @@
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/tv_path"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/server_lab_path" /> android:text="@string/server_lab_path" />

Some files were not shown because too many files have changed in this diff Show More