Compare commits

..

311 Commits

Author SHA1 Message Date
2dust 13480b0077 up1.7.21 2022-10-14 20:33:10 +08:00
2dust 1987880294 Merge pull request #1689 from BI7PRK/master
Add uTLS option
2022-10-14 20:01:32 +08:00
xskill 0ae459a70e Add uTLS option 2022-10-13 15:07:10 +08:00
2dust 02bc46634d Update README.md 2022-10-10 20:24:52 +08:00
2dust cfdaa0c54b Update build.gradle 2022-09-19 20:42:01 +08:00
2dust cbe6c1e3e0 Update build.gradle 2022-09-19 20:25:48 +08:00
2dust 3cd6f12b94 Merge pull request #1639 from zhaoguomanong/master
fix NullPointerException
2022-09-10 19:51:19 +08:00
zhaoguomanong 516235cd6a fix NullPointerException
复现方法:
添加两个订阅A, B -> 选择A中任意节点并启动 -> 删除A订阅, 选中B订阅中任意节点启动 -> crash

09-09 10:53:50.355 18739 18739 D AndroidRuntime: Shutting down VM
09-09 10:53:50.356 18739 18739 E AndroidRuntime: FATAL EXCEPTION: main
09-09 10:53:50.356 18739 18739 E AndroidRuntime: Process: com.v2ray.ang, PID: 18739
09-09 10:53:50.356 18739 18739 E AndroidRuntime: java.lang.NullPointerException
09-09 10:53:50.356 18739 18739 E AndroidRuntime: 	at com.v2ray.ang.ui.MainRecyclerAdapter.onBindViewHolder$lambda-6(MainRecyclerAdapter.kt:156)
09-09 10:53:50.356 18739 18739 E AndroidRuntime: 	at com.v2ray.ang.ui.MainRecyclerAdapter.$r8$lambda$VmDbsAxtrWNiVtS0BmO3UDui2o4(Unknown Source:0)
09-09 10:53:50.356 18739 18739 E AndroidRuntime: 	at com.v2ray.ang.ui.MainRecyclerAdapter$$ExternalSyntheticLambda2.onClick(Unknown Source:4)
09-09 10:53:50.356 18739 18739 E AndroidRuntime: 	at android.view.View.performClick(View.java:6597)
09-09 10:53:50.356 18739 18739 E AndroidRuntime: 	at android.view.View.performClickInternal(View.java:6574)
09-09 10:53:50.356 18739 18739 E AndroidRuntime: 	at android.view.View.access$3100(View.java:778)
09-09 10:53:50.356 18739 18739 E AndroidRuntime: 	at android.view.View$PerformClick.run(View.java:25906)
09-09 10:53:50.356 18739 18739 E AndroidRuntime: 	at android.os.Handler.handleCallback(Handler.java:873)
09-09 10:53:50.356 18739 18739 E AndroidRuntime: 	at android.os.Handler.dispatchMessage(Handler.java:99)
09-09 10:53:50.356 18739 18739 E AndroidRuntime: 	at android.os.Looper.loop(Looper.java:193)
09-09 10:53:50.356 18739 18739 E AndroidRuntime: 	at android.app.ActivityThread.main(ActivityThread.java:6718)
09-09 10:53:50.356 18739 18739 E AndroidRuntime: 	at java.lang.reflect.Method.invoke(Native Method)
09-09 10:53:50.356 18739 18739 E AndroidRuntime: 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:491)
09-09 10:53:50.356 18739 18739 E AndroidRuntime: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

Signed-off-by: zhaoguomanong <zhaoguomanong@gmail.com>
2022-09-09 13:12:15 +08:00
2dust 236923d0a0 Update build.gradle 2022-08-30 14:53:13 +08:00
2dust a4816b8251 Update build.gradle 2022-08-30 14:53:06 +08:00
2dust 2c2bc86457 Update build.gradle 2022-08-13 08:34:43 +08:00
2dust 68a03a93b5 Merge pull request #1618 from zhaoguomanong/master
Fix app crashing during updating subscribe & translation error
2022-08-12 20:11:34 +08:00
2dust 5127a30ae9 Merge pull request #1617 from Fe3O4-Git/patch-1
tweak toast in UserAssetActivity.kt
2022-08-12 20:02:40 +08:00
zhaoguomanong 8b76a7a4f4 avoid modifying serversCache serverList from multithreading
Bug: app force close during updating subscribe

Reroduce steps: quickly scroll up and down the RecyclerView during updating subscribe
if the server list length is more than the screen height, the bug can be 100% reproduced

Solution: reloadServerList from UI Thread

Signed-off-by: zhaoguomanong <zhaoguomanong@gmail.com>
2022-08-12 15:15:32 +08:00
zhaoguomanong b6aec3fd63 fix translation error
Signed-off-by: zhaoguomanong <zhaoguomanong@gmail.com>
2022-08-12 11:09:23 +08:00
LiAlH4 18e0dc4546 fix 2022-08-12 08:07:46 +08:00
LiAlH4 129c1db995 tweak toast in UserAssetActivity.kt
add missing space
2022-08-12 08:06:06 +08:00
2dust 56d987713e Update build.gradle 2022-07-24 07:46:57 +08:00
2dust 35e57977ea Update build.gradle 2022-07-24 07:46:34 +08:00
2dust 34c14d1f1a Merge pull request #1586 from yuhan6665/master
Open source v1.7.15
2022-07-09 09:25:21 +08:00
yuhan6665 bce08c5136 Update readme 2022-07-08 20:05:29 -04:00
yuhan6665 2d90a07788 Ignore *.aar 2022-07-08 20:05:29 -04:00
yuhan6665 b4204ed3a8 Add aar lib from Xray-core latest main branch
This is just a convenience for developer to work with the project
2022-07-08 20:05:13 -04:00
yuhan6665 d30fb697bb Open source v1.7.15
Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2022-07-08 20:02:34 -04:00
2dust 18ff11dbba Update .gitignore 2022-07-05 15:01:56 +08:00
2dust 3923b379a6 Merge pull request #1563 from yuhan6665/ss-fix
Fix ss2022 import for old base64 format
2022-06-26 08:43:46 +08:00
yuhan6665 e23e9c48a4 Fix ss2022 import for old base64 format
Old format has userinfo = null, will throw NPE
2022-06-24 23:43:37 -04:00
2dust 1822613985 add xchacha20 2022-06-24 23:42:08 -04:00
2dust e8ec9ad17c Merge pull request #1555 from yuhan6665/ss-fix
Fix ss2022 import for multi clients format
2022-06-19 08:39:08 +08:00
yuhan6665 b4d970552e Fix ss2022 import for multi clients format 2022-06-17 20:30:39 -04:00
2dust c6579556c4 Support 2022-blake3 share 2022-06-17 20:25:43 -04:00
2dust 5a36844036 add Shadowsocks-2022 for xray-core 2022-06-17 20:25:43 -04:00
2dust 2b21203c53 Update AngConfigManager.kt 2022-06-17 20:24:54 -04:00
2dust a4558cf954 Add support for sharing ipv6 addresses 2022-06-17 20:24:29 -04:00
2dust 5c2f11fb19 Merge pull request #1547 from emp3826/master
update dependences
2022-06-10 20:32:32 +08:00
cjhhz d7f3d0df80 update dependences 2022-06-10 13:08:20 +08:00
2dust c5e2ca0d8d Merge pull request #1495 from cuynu/master
Improve Vietnamese translation
2022-05-08 18:31:12 +08:00
Cuynu 252bea2432 Add files via upload 2022-05-07 18:25:53 +07:00
2dust f58ed74a4a add external res 2022-05-07 15:29:36 +08:00
2dust 8ed17f9da0 Merge pull request #1446 from pozhiloy-enotik/patch-1
Allow external use
2022-04-15 07:21:23 +08:00
pozhiloy-enotik 1bbfda64fe Allow external use 2022-04-12 16:18:34 +03:00
2dust 5cadef8b2a Merge pull request #1416 from yuhan6665/master
Refactor and remove Kotlin synthetics
2022-03-20 10:16:16 +08:00
yuhan6665 5b92158353 Update readme 2022-03-19 21:27:26 -04:00
yuhan6665 73706c1d0f Refactor and remove Kotlin synthetics 2022-03-19 21:21:44 -04:00
2dust c633a267ff Merge pull request #1393 from yuhan6665/master
Update readme and sync project
2022-02-27 08:55:34 +08:00
yuhan6665 c8e5bf4f9f Update project 2022-02-26 12:15:46 -05:00
2dust bb91b3baa9 onModeHelpClicked 2022-02-26 12:05:02 -05:00
2dust 35dc8d661c Update AndroidManifest.xml 2022-02-26 12:05:02 -05:00
2dust f61f30fdc4 change sdk version 2022-02-26 12:05:02 -05:00
2dust a54c327a07 Merge pull request #1374 from Kaitul/patch-1
Update strings.xml
2022-02-26 19:21:22 +08:00
人工知能 87d2854fb2 Update strings.xml 2022-02-15 12:48:44 +08:00
2dust e9b1052ef7 Merge pull request #1336 from yuhan6665/remove-alterid
Remove alterid
2022-01-09 18:04:08 +08:00
yuhan6665 c6560e9bc0 Remove alterId field
VmessQRCode "aid" is kept for backwards compatibility and always set to 0
Since 2022, new *ray server will reject alterId > 0.
If it is enabled and set specifically > 0, for v2fly server v4.28.1+ and Xray server v1.0.0+, client with either 0 or above 0 can connect
For older v2ray server, the default config will not work, user has to use custom config to set alterId. This is NOT recommended for security reason
2022-01-07 18:30:11 -05:00
yuhan6665 a44ca16aa7 Fix some compile warnings 2022-01-07 18:30:11 -05:00
2dust b28c7f54ff Merge pull request #1333 from xiandanin/master
更新xray-core版本
2022-01-06 19:37:50 +08:00
xiandanin 61a155b799 更新xray-core版本 2022-01-05 20:25:45 +08:00
2dust 10cc117e81 Merge pull request #1268 from yuhan6665/new-fakedns
New config format
2021-10-30 19:02:26 +08:00
yuhan6665 ce9bed2e1f New config format 2021-10-29 20:37:12 -04:00
2dust bb8f7de6eb Merge pull request #1263 from yuhan6665/new-dns
Add support for DOT and DOQ
2021-10-24 19:51:47 +08:00
yuhan6665 a1ed4836c7 Add support for DOT and DOQ 2021-10-23 20:24:08 -04:00
2dust 2d5351ec9e Merge pull request #1218 from yuhan6665/fix-sniffing2
Fix sniffing default value again
2021-09-07 20:02:52 +08:00
yuhan6665 d9fb121d67 Fix sniffing default value again
I made a mistake, thought it would return null when key is not in storage,
But it is found default "true" must be specified
2021-09-03 18:15:06 -04:00
2dust 62d0951a24 Merge pull request #1208 from yuhan6665/fix-viewpager
Fix an issue with ViewPage2 and menu bar
2021-08-28 17:06:21 +08:00
2dust d6605cc866 Merge pull request #1204 from douo/master
Add support in subscription url for http basic authentication.
2021-08-28 17:06:10 +08:00
yuhan6665 1fc493d879 Fix an issue with ViewPage2 and menu bar 2021-08-27 17:48:45 -04:00
tiou bdc27dd180 Use built-in urlDecode. 2021-08-23 16:58:48 +08:00
tiou e257c4cb56 Subscription support http basic authentication in url. 2021-08-23 16:49:35 +08:00
2dust e9d2ed98af Merge pull request #1198 from yuhan6665/user-agent
Set user agent to v2rayNG/version when update subscription
2021-08-21 19:19:20 +08:00
yuhan6665 901a82eb54 Set user agent to v2rayNG/version when update subscription 2021-08-20 18:29:34 -04:00
2dust 26cc29944b Merge pull request #1183 from yuhan6665/fix-drag
Fix a bug when user delete item right after drag it
2021-08-17 20:20:56 +08:00
yuhan6665 7052546f2b Fix a bug when user delete item right after drag it 2021-08-13 18:04:43 -04:00
2dust ae2b10ede7 Merge pull request #1177 from yuhan6665/fix-base64
Fix parsing for some loosely formatted base64
2021-08-10 13:09:35 +08:00
2dust 0df051e640 Merge pull request #1175 from jiuqianyuan/master
Add prompts for update subscription error
2021-08-10 13:06:14 +08:00
jiuqianyuan 79aa86a402 Update MainActivity.kt 2021-08-09 17:22:49 +08:00
yuhan6665 7bf32c2b30 Fix parsing for some loosely formatted base64 2021-08-07 17:35:09 -04:00
jiuqianyuan ceb1c55e49 Fix missing prompts for update subscription error 2021-08-07 08:08:45 +08:00
2dust 4ab3134eac Merge pull request #1164 from yuhan6665/update
Fix compile warnings
2021-07-31 15:38:19 +08:00
yuhan6665 0391685d42 Fix some more compile warnings 2021-07-30 18:31:29 -04:00
yuhan6665 6482253697 Fix some compile warnings
remove flatDir
2021-07-30 18:31:29 -04:00
yuhan6665 c033358126 Fix some compile warnings
Migrate to Androidx Activity Result APIs
2021-07-30 18:31:29 -04:00
yuhan6665 0b52c78b72 Fix some compile warnings
Some deprecated methods
2021-07-30 18:31:29 -04:00
yuhan6665 a2a7d790e7 Fix some compile warnings
Migrate to viewpager2
2021-07-30 18:31:29 -04:00
yuhan6665 c7c6564624 Fix some compile warnings
upper case and lower case
2021-07-30 18:31:29 -04:00
2dust f7d534fc00 Merge pull request #1153 from yuhan6665/update
Migrate to View Binding
2021-07-26 08:07:28 +08:00
yuhan6665 ecb1d58e7a Migrate to View Binding
Use view binding to simplify some code
2021-07-23 18:17:17 -04:00
2dust 54a191d181 Merge pull request #1147 from yuhan6665/update
Update project dependencies
2021-07-20 08:12:30 +08:00
2dust 90b6979f94 Merge pull request #1146 from jiuqianyuan/master
fix update subscription error
2021-07-20 08:12:18 +08:00
jiuqianyuan 5daf4886b7 fix wrong commit
wrong commit for dbee208
2021-07-17 16:18:33 +08:00
jiuqianyuan dbee2085fb clean useless “” 2021-07-17 15:58:29 +08:00
yuhan6665 3c6c4ca3a5 Update project dependencies 2021-07-16 18:11:28 -04:00
jiuqianyuan 3932ee6e9b fix update subscription error
Prevent server list from being emptied when update subscription failed or the subscription url is invalid
2021-07-16 17:49:56 +08:00
2dust 242c96a4de Merge pull request #1136 from yuhan6665/EditorKit
Editor kit
2021-07-14 08:26:14 +08:00
yuhan6665 a23245435f Use EditorKit for custom config
https://github.com/massivemadness/Squircle-IDE
2021-07-09 20:45:48 -04:00
yuhan6665 57476290d3 Auto Migrate to AndroidX 2021-07-09 20:44:38 -04:00
2dust c5617ac65a Merge pull request #1129 from yuhan6665/fix-host
Fix host should be empty list when it is blank
2021-07-03 16:48:12 +08:00
yuhan6665 3795348b04 Fix host should be empty list when it is blank 2021-07-02 17:40:15 -04:00
2dust 50bf9c8da0 Merge pull request #1116 from yuhan6665/fix-base64
Fix base64-url-safe format decode
2021-06-27 11:52:20 +08:00
yuhan6665 78fe4e4bc4 Fix base64-url-safe format decode 2021-06-26 19:44:21 -04:00
2dust 962519854d Merge pull request #1092 from yuhan6665/Logtag
Log with consistent tag
2021-06-13 19:53:46 +08:00
yuhan6665 841629f9a3 Cleanup some files 2021-06-12 23:38:31 -04:00
yuhan6665 d32ccc817a Log with consistent tag 2021-06-12 23:38:31 -04:00
2dust 19cc665f2d Merge pull request #1088 from yuhan6665/import-fix
Fix a null pointer when subscribe link doesn't have sni field
2021-06-07 08:17:13 +08:00
yuhan6665 031e9105e2 Fix a null pointer when subscribe link doesn't have sni field 2021-06-06 11:18:58 -04:00
2dust 35c5d64863 Merge pull request #1070 from yuhan6665/bypass-fix
bypass local also bypass multicast address
2021-05-29 10:29:25 +08:00
yuhan6665 0b05756d12 bypass local also bypass multicast address
https://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.xhtml
2021-05-28 19:51:08 -04:00
2dust 3548dcbb67 Merge pull request #1065 from yuhan6665/domestic
Always add domestic dns to config
2021-05-24 08:14:43 +08:00
yuhan6665 b897b2a0e9 Always add domestic dns to config
Previously, domestic dns is only added to config under
"local dns mode". However, it should be used for V2ray
core routing DNS as well.
2021-05-23 11:56:23 -04:00
2dust eb60e4a0e4 Merge pull request #1058 from yuhan6665/grpc
Grpc
2021-05-16 19:35:16 +08:00
yuhan6665 c3dfa8cedc Support gRPC import and export
New Format https://github.com/XTLS/Xray-core/issues/91
For Vmess QRcode https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
gRPC mode is mapped to "type"
2021-05-15 23:18:09 -04:00
yuhan6665 f58bf85b6d Update UI for gRPC
Separate transport header types for different networks
Change UI dynamically based on user selection of network
2021-05-15 23:04:58 -04:00
2dust 906d0714b5 Merge pull request #1050 from yuhan6665/fakedns
Fakedns
2021-05-08 11:28:42 +08:00
yuhan6665 701fed2525 Update settings UI, add new VPN section 2021-05-07 21:55:44 -04:00
yuhan6665 e83208465f Add vpn dns preference setting
Vpn Dns can now be changed separately from remote Dns
2021-05-07 21:17:11 -04:00
yuhan6665 2b031033d3 Add fake dns preference setting 2021-05-07 20:55:02 -04:00
2dust e0e16b5934 fix tcp http request 2021-05-07 20:50:52 -04:00
2dust 0748f994ef Merge pull request #1038 from yuhan6665/new-storage-fix
New storage fix
2021-05-01 18:20:11 +08:00
yuhan6665 233b34bda6 Fix scrolling by cache server config in ViewModel 2021-04-30 20:17:41 -04:00
yuhan6665 6ece3385fe Fix trojan sni saved as null when it should be empty 2021-04-30 20:17:41 -04:00
2dust 9b8a810445 Merge pull request #1028 from yuhan6665/master
Update tun2socks
2021-04-26 08:22:47 +08:00
yuhan6665 f63242d147 Update tun2socks
https://github.com/XTLS/badvpn/commit/60a66a363f78def8831309cff0932c0bd5535e8e
compiled with NDK 22.1.7171670
2021-04-25 19:56:14 -04:00
2dust 7024fabb82 Merge pull request #1016 from yuhan6665/new-storage-fix
Fix data migration sniff setting default to true
2021-04-17 10:24:19 +08:00
2dust 459f52fec6 Merge pull request #1015 from yuhan6665/xray
Update AndroidLibrayLite project to Xray
2021-04-17 10:24:10 +08:00
yuhan6665 90f2d33d97 Fix data migration sniff setting default to true 2021-04-16 20:33:59 -04:00
yuhan6665 115008a8a4 Update AndroidLibrayLite project to Xray 2021-04-16 09:17:58 -04:00
2dust 8cdc7fb3c9 Merge pull request #1001 from yuhan6665/new-storage-fix
Rollback parsed custom config
2021-04-11 19:28:47 +08:00
yuhan6665 d0a2fa0086 Rollback parsed custom config
To minimize change, the data structure for ServerConfig still stay the same
Add another table for server raw config
2021-04-10 21:30:34 -04:00
2dust f6c54841d2 Merge pull request #990 from yuhan6665/new-storage-fix
New storage fix
2021-04-05 20:21:51 +08:00
yuhan6665 54fa356999 Add some new config for v2fly 4.37.0 2021-04-04 19:25:52 -04:00
2dust 9642b7f64f up grpc 2021-04-04 19:25:52 -04:00
yuhan6665 dd2d2c1638 Add some missing config from Xray 2021-04-04 19:25:51 -04:00
yuhan6665 e21950dbcd Improve custom config error toast 2021-04-04 19:25:51 -04:00
2dust d016ab06d4 Merge pull request #973 from yuhan6665/new-storage-fix
New storage fix
2021-03-29 08:06:43 +08:00
yuhan6665 4d9aced5a4 Gson display to not escape HTML character like "=" 2021-03-28 11:34:41 -04:00
yuhan6665 62b928e6a0 Support Trojan flow and email 2021-03-28 11:34:34 -04:00
2dust 0ce60eae73 Update MainRecyclerAdapter.kt 2021-03-28 11:34:12 -04:00
2dust 5930a6a9eb Update V2rayConfig.kt 2021-03-28 11:34:01 -04:00
2dust a360310be2 fix header type none 2021-03-28 11:33:51 -04:00
2dust 820e6cdf36 fix migration inbound port parsed as 0 2021-03-28 11:31:18 -04:00
2dust 658b890325 Merge pull request #965 from yuhan6665/new-storage
New storage
2021-03-27 09:42:12 +08:00
yuhan6665 fb017c6659 Add migration
- Fix some compile warnings
- Add migration method
- Add vmessBean migration
2021-03-26 20:55:00 -04:00
yuhan6665 00e6314afe Merge branch 'master' into new-storage 2021-03-26 20:55:00 -04:00
yuhan6665 463f45804f Add missing data fields for config
Make custom config able to parse any config
Fix subscription TODO
Fix some bugs
- vless tls should clear out flow
- trojan tls settings
- import with null rawQuery link
- socks share link username
- remove an item in the middle of server list
2021-03-26 20:55:00 -04:00
yuhan6665 572955dd1e Move everything else to MMKV
- Exclude old armv5 so lib from MMKV
- Move subscription to MMKV
- Clean up settings key
- Map settings to MMKV
- Say goodbye to DPreference

Fix some bugs:
- null pointer check
- server list refresh
- show test result
- custom config display
- fix port number in DNS object shown as Double
- shadowsocks, socks streamsettings
- quic settings with wrong security
- main list footer
2021-03-20 01:00:12 -04:00
yuhan6665 375a209beb Move main storage references to MMKV
- Move current index to MMKV
- Move ServiceManager to MMKV
- Fix isRunning in ServerActivity
- Drop support for VMESS qrcode v1
- Change all protocols importing
2021-03-19 23:23:10 -04:00
yuhan6665 872f9ce199 Add MMKV and data class
- Move ServerActivity to MMKV
- Move ServerCustomConfigActivity to MMKV
- Cleanup ServerActivities
- Cleanup V2rayConfigUtil
- Change data reads in RecyclerAdapter and ConfigManager
- Cleanup some constants
2021-03-19 23:23:09 -04:00
2dust b4f02c9bd6 Merge pull request #894 from Humilton/master
Support keyboard navigation for fab
2021-02-21 12:56:05 +08:00
hubaozhong e567719f5b Support keyboard navigation for fab
Signed-off-by: hubaozhong <d63hbz@gmail.com>
2021-02-18 23:01:31 +08:00
2dust 8407fc5825 Merge pull request #866 from yuhan6665/trim-r
Fix the string list trim
2021-02-01 08:05:37 +08:00
yuhan6665 a3e49dcc3d Fix the string list trim 2021-01-30 21:59:54 -05:00
2dust 7b47bbe99a Merge pull request #846 from yuhan6665/list-ui
Add subscription remarks in server list
2021-01-16 13:18:27 +08:00
yuhan6665 0fb2165015 Add subscription remarks in server list 2021-01-15 20:44:28 -05:00
2dust 03eeeb9b62 Merge pull request #827 from yuhan6665/master
Update readme
2021-01-02 09:18:45 +08:00
yuhan6665 038daf5fda Update readme 2021-01-01 09:21:10 -05:00
2dust bfd1387d9b Merge pull request #817 from yuhan6665/fix-manual-update
Fix manual update
2020-12-26 12:03:38 +08:00
yuhan6665 5afec5cf25 Update full config when edit manually 2020-12-25 18:34:58 -05:00
yuhan6665 ec29bdf5bf Refactor for re-use genStoreV2rayConfig() 2020-12-25 18:34:58 -05:00
2dust 57efab093f Merge pull request #803 from yuhan6665/fix-712
Fix toast BadTokenException in OS 7.1.2
2020-12-14 08:21:51 +08:00
2dust 9c92a64811 Merge pull request #801 from yuhan6665/fix-oculus
Fix widget manager null pointer in Oculus
2020-12-14 08:21:41 +08:00
yuhan6665 7ddc82d5cd Fix toast BadTokenException in OS 7.1.2
Apparently recent changes with ViewModel affect the internal of
Activity which lead to toast throwing BadTokenException in OS
7.1.2.
The error is not easily catchable. This library use reflection
to override a key function in WindowManager to catch the error.
I have audit the code of the library.

See https://github.com/PureWriter/ToastCompat for more details
2020-12-13 18:45:04 -05:00
yuhan6665 c286ba18a8 Fix widget manager null pointer in Oculus 2020-12-12 23:11:00 -05:00
2dust 867b5fc880 Merge pull request #795 from rurirei/v2init
init update
2020-12-10 08:25:10 +08:00
rurirei e8a7fa5320 create init 2020-12-09 17:35:09 +08:00
rurirei f2f9e55286 do not init all 2020-12-09 17:32:50 +08:00
2dust 4a1c62a67c Merge pull request #787 from yuhan6665/fix-selected
Fix update full config when settings change
2020-12-06 13:48:03 +08:00
yuhan6665 c9a6a459d4 Fix update full config when settings change
Now daemon process does not reference the node list at all and
only depend on a couple of settings like PREF_CURR_CONFIG..
2020-12-05 23:42:44 -05:00
2dust 21fdcf4ccf Merge pull request #783 from yuhan6665/fix-selected
Fix select node of two processes in sync
2020-12-05 18:20:11 +08:00
yuhan6665 7c7a623ae5 Fix select node of two processes in sync
As proxy only mode is added in 1.4.0, I moved the toggle components to the daemon process
When user start service from toggle, the full config is generated from a cached list.
However, this cache is not in sync with the main process list.
In fact, we don't need to generate full config right before the service start. We only
need to when active node is changed.
This way, code logic and daemon process is kept simple
2020-12-04 22:37:31 -05:00
2dust b3074e9697 Merge pull request #763 from yuhan6665/memory-optimization
Slightly improve memory by reduce unnecessary DPreference usage
2020-11-29 09:31:54 +08:00
yuhan6665 513ebcfa23 Slightly improve memory by reduce unnecessary DPreference usage
See more details in _Ext.kt.
In the future, change will be made to our config storage, so that
the service started through TileService/Widget/ScSwitchActivity will
also not launch main process. That will greatly reduce memory usage
2020-11-28 18:08:50 -05:00
2dust 50d9057f1a Merge pull request #750 from yuhan6665/viewmodel-test
Add ViewModel
2020-11-16 13:22:08 +08:00
2dust 2a563e7884 Merge pull request #747 from liyufan/new_template
Use new issue template
2020-11-16 13:21:40 +08:00
2dust c69cd18842 Merge pull request #732 from rurirei/isTun2socksRunningi
isTun2socksRunning bool
2020-11-16 13:20:48 +08:00
2dust 7f2ced85a8 Merge pull request #730 from rurirei/stoploop
stoploop update
2020-11-16 13:18:46 +08:00
yuhan6665 6c5eef99b5 Move tests and broadcast listener to ViewModel 2020-11-15 18:37:46 -05:00
yuhan6665 d7c3bae8cc Add MainViewModel
ViewModel is the recommend approach for asynchronous loading
for Activity.
The variable stays even if the Activity is killed temporarily.
2020-11-15 18:37:46 -05:00
LYF 57c98f7c50 Use new issue template 2020-11-13 22:21:35 +08:00
rurirei 49be23c56a bool in return 2020-11-08 11:40:02 +08:00
rurirei 9b658e9a22 IsTun2socksRunning bool 2020-11-08 11:34:49 +08:00
rurirei aaa84d081f stoploop update 2020-11-08 10:53:23 +08:00
2dust 5628cbee3a Merge pull request #725 from mzz2017/patch-1
fix: the problem that importing SS URL not support standard format
2020-11-05 14:30:56 +08:00
2dust 3dd663d927 Merge pull request #698 from yuhan6665/master
Release AndroidLibV2rayLite 22
2020-11-05 14:30:41 +08:00
mzz 9e49a2dbd9 fix: the problem that importing SS URL not support standard format 2020-11-02 12:37:26 +08:00
2dust 6c199c1687 Merge pull request #662 from yuhan6665/ext-geo
Add external geofile replacement help
2020-10-24 13:35:21 +08:00
yuhan6665 39af5fdb86 Release AndroidLibV2rayLite 22 2020-10-21 21:31:23 -04:00
2dust 7b2794f6be Merge pull request #695 from rurirei/proxyOnly
proxyOnly update
2020-10-22 07:59:19 +08:00
2dust 411d9e5c9a Merge pull request #691 from rurirei/api30
target 30
2020-10-22 07:58:41 +08:00
Rurirei a57aee9424 proxyOnly update
* do not run Tun2socks under proxyOnly
2020-10-21 15:19:55 +08:00
Rurirei 4602afc67e call stopV2ray on onDestroy 2020-10-21 15:14:44 +08:00
Rurirei ceb29840f2 BuildTools 30 fix 2020-10-20 09:49:16 +08:00
Rurirei 1c1f130ca7 update badge 2020-10-20 09:42:48 +08:00
Rurirei afa0eb9375 package visibility 2020-10-20 07:20:37 +08:00
Rurirei 49b682f0f3 target 30
* update dependencies
2020-10-20 07:10:19 +08:00
Rurirei d5def3bf2f useless anko 2020-10-20 07:06:36 +08:00
2dust 51b97b64f2 Merge pull request #670 from yuhan6665/fix-gson
Fix crash when custom config cannot be parsed by Gson
2020-10-15 08:18:59 +08:00
2dust 3af9ce1a1a Merge pull request #657 from rurirei/native
native fixes
2020-10-15 08:18:48 +08:00
yuhan6665 a5d3dda941 Fix crash when custom config cannot be parsed by Gson 2020-10-14 11:28:32 -04:00
yuhan6665 71a3e300d8 Add external geofile replacement help 2020-10-09 21:34:15 -04:00
Rurirei 030b9a3900 native fixes
* call sendFd() before tun2socks start
2020-10-08 15:50:51 +08:00
Rurirei 24d105a53c catch logs to checkout this error 2020-10-08 15:49:30 +08:00
2dust 45805d6df7 Merge pull request #644 from yuhan6665/clean-dependency
Clean dependency
2020-10-04 19:16:27 +08:00
2dust 4437e6699b Merge pull request #635 from rurirei/diff
issues of VPNService.kt
2020-10-04 19:16:14 +08:00
2dust fa409f91e4 Merge pull request #626 from yuhan6665/go
Clean up tun2socks
2020-10-04 19:16:00 +08:00
yuhan6665 89be5f077b Say goodbye to Anko
replace with support lib PreferenceManager
replace LayoutInflater
2020-10-03 11:19:46 -04:00
yuhan6665 896889778f Replace Anko with support lib AlertDialog
replace some property setter
2020-10-03 11:19:46 -04:00
yuhan6665 10f705a8b2 Use standard startActivity 2020-10-03 11:19:46 -04:00
yuhan6665 2956fa2030 Replace doasync with Kotlin coroutine
Using a custom coroutine scope, we don't need to keep track of the
running job. We can simply call cancel children for the scope.
2020-10-03 11:19:46 -04:00
yuhan6665 c2a704a6ea Deprecate some anko function
- Replace toast with inline function
- Use standard startActivityForResult
2020-10-03 11:19:46 -04:00
yuhan6665 78cac0cd90 Replace divider with standard library
Also, cleanup some unused classes
2020-10-03 11:19:46 -04:00
Rurirei 3ffb2e8e05 catch exception for lateinit
PropertyNotInitialized exception needs catch for lateinit var
2020-10-03 20:23:43 +08:00
Rurirei e2d667e0bb sendFd() should not called twice 2020-10-01 17:36:07 +08:00
Rurirei 3ae0777d7f useless bool 2020-10-01 17:07:51 +08:00
Rurirei 5e6348676c ignore err of registerNetworkConnectivity 2020-10-01 15:32:08 +08:00
2dust df3f1ca3ef Merge pull request #620 from yuhan6665/up-version
Update dependencies
2020-09-29 08:05:46 +08:00
yuhan6665 94f2bec329 Clean up tun2socks 2020-09-27 23:17:47 -04:00
yuhan6665 c54d8fa43a Update dependencies
Fix a delegate annotation for Kotlin 1.4
2020-09-25 18:37:29 -04:00
2dust 5bbbdcf6f2 Merge pull request #610 from yuhan6665/fix-protect
Fix protect() when there is no service
2020-09-21 07:59:55 +08:00
yuhan6665 4abf20fa32 Fix protect() when there is no service 2020-09-20 19:00:14 -04:00
2dust 723727feb9 Merge pull request #607 from yuhan6665/help-proxy-mode
Help proxy mode
2020-09-19 18:56:15 +08:00
yuhan6665 2efd4b741c Add badge, wiki link and dev guide 2020-09-18 17:48:54 -04:00
yuhan6665 83aab0f880 Add help link in mode setting 2020-09-18 17:48:54 -04:00
2dust 66ea17877e Merge pull request #598 from yuhan6665/fix-process
Add missing process for ScSwitchActivity
2020-09-12 19:48:04 +08:00
2dust 26bc985368 Merge pull request #597 from yuhan6665/settings-ui
Settings ui
2020-09-12 19:47:48 +08:00
yuhan6665 5bbf40c784 Grey out per-app proxy in proxy only mode 2020-09-11 21:30:08 -04:00
yuhan6665 6d5c23245c Move version to drawer
Make it easier to check version.
Technically, version should not be selectable setting
2020-09-11 21:30:08 -04:00
yuhan6665 b148290211 Add missing process for ScSwitchActivity 2020-09-11 21:29:58 -04:00
2dust c473f9bb13 Merge pull request #587 from yuhan6665/proxy-mode
Proxy only mode
2020-09-06 08:09:10 +08:00
2dust c7c3d27f36 Merge pull request #584 from akiirui/fix-metered
V2RayVpnService.kt: fix OS marks VPN as metered
2020-09-06 08:08:58 +08:00
yuhan6665 bebc6fea13 Add mode in settings 2020-09-04 18:19:51 -04:00
yuhan6665 9271857b1e Remove VPN related logic in toggle components 2020-09-04 18:19:51 -04:00
yuhan6665 7ea78c1840 Make all toggle component run in daemon process
This commit make the two process with clear role:
 - Daemon process will run proxy core, keep minimum configuration
 and long running in the background, support toggle on/off
 - Main process will have all UI, all servers configuration
2020-09-04 18:19:51 -04:00
yuhan6665 ac668788b3 Add proxy only service 2020-09-04 18:19:51 -04:00
yuhan6665 adfcf0a5d9 Fix some trivial IDE warnings 2020-09-04 18:19:51 -04:00
yuhan6665 15b5595797 Refactor service
Break VpnService so that it only control vpn and tun2socks,
Other function is moved to a singleton service manager.
This code makes it possible to add proxy only service next.
2020-09-04 18:19:51 -04:00
Akatsuki 5a18296cb2 V2RayVpnService.kt: use setMetered
setMetered to false let VPN network to inherit its meteredness from its underlying networks.

Revert `addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)`
2020-09-03 02:07:11 +08:00
Akatsuki 1256edbaf5 V2RayVpnService.kt: fix OS marks VPN as metered
VPN apps targeting Build.VERSION_CODES.Q or above will be considered metered by default.

Ref:
https://developer.android.com/reference/android/net/VpnService.Builder#setMetered(boolean)
https://developer.android.com/about/versions/pie/android-9.0-changes-all#network-capabilities-vpn
2020-09-02 13:46:18 +08:00
2dust 870950e807 Merge pull request #577 from yuhan6665/fix-target-29
Fix release build with tun2socks
2020-08-30 07:21:24 +08:00
yuhan6665 e798cb3f42 Fix release build with tun2socks 2020-08-29 12:34:54 -04:00
2dust b970f0bcff Merge pull request #574 from yuhan6665/all-outbounds-speed
All outbounds speed
2020-08-29 20:33:18 +08:00
2dust 93b9a56428 Merge pull request #387 from yuhan6665/target-29
Target sdk 29
2020-08-29 20:32:59 +08:00
yuhan6665 eb8bd13266 Add speed notification for all outbounds 2020-08-28 21:52:44 -04:00
yuhan6665 d850c88c63 Support ordered set in DPreference
Note the ordered set need to be stored internally as String,
not string set
2020-08-28 21:52:43 -04:00
yuhan6665 6bee795c0d Update target sdk to 29 2020-08-27 20:19:00 -04:00
yuhan6665 d7d7e029e0 Fix compile warnings that will be error in sdk 29 2020-08-27 20:18:59 -04:00
2dust 8efdab43d7 Merge pull request #566 from yuhan6665/array-crash
Fix test crash if node is removed
2020-08-22 08:06:36 +08:00
yuhan6665 5e0235cf70 Fix test crash if node is removed
This should fix almost all issues when the testing is in progress
and the array is changed. However, there might be uncovered edge
cases since the vmess array is in both process and accessed by
multiple threads.
2020-08-21 19:47:33 -04:00
2dust 9f668b3da7 Merge pull request #559 from yuhan6665/custom-config-improve
Custom config improve
2020-08-16 20:26:17 +08:00
yuhan6665 b2437279dc Update UI for custom config
- add server address and port
- add share button (full configuration)
- update "Export all config to clipboard" text to indicate
it will not include custom config
2020-08-15 19:37:59 -04:00
yuhan6665 180b5efd93 Enable tcping test for custom config 2020-08-15 19:37:59 -04:00
yuhan6665 dc31380cc2 Refactor EConfigType to be an enum
Using enum will simplify logic and conveniently providing Int value
and a String name
2020-08-15 19:37:59 -04:00
2dust 1361e0dacf Merge pull request #555 from yuhan6665/per-app-list
Update latest per-app list
2020-08-15 20:40:10 +08:00
2dust a45eb66bd1 Merge pull request #554 from ShrdBB/master
Fix typo in strings.xml
2020-08-15 20:39:50 +08:00
yuhan6665 508102cebe Update latest per-app list
https://github.com/2dust/androidpackagenamelist/blob/master/proxy.txt
2020-08-14 20:45:30 -04:00
ShrdBB 1d86dbb9f3 fix typo in strings.xml 2020-08-14 12:48:46 +08:00
2dust 20ca554be2 Merge pull request #544 from yuhan6665/android-system
Add "Android System" in per-app vpn
2020-08-09 12:57:49 +08:00
yuhan6665 25ba455656 Add "Android System" in per-app vpn 2020-08-08 23:49:06 -04:00
2dust 17e7c62d53 Merge pull request #542 from yuhan6665/master
Sanitize project configuration
2020-08-08 15:35:16 +08:00
yuhan6665 6f0f2fdeda Sanitize project configuration
Make project compilable out of box. This commit address
the following:
- remove release key
- fix gitignore for Android Studio temp files
- add gradle wrapper properties
2020-08-07 21:37:00 -04:00
2dust 3c9c9b5a4c Merge pull request #531 from yuhan6665/emui-notification
Fix notification for Emui 4.1
2020-08-06 20:40:45 +08:00
2dust e13024d6bb Merge pull request #526 from Vixb1122/master
exclude ScSwitchActivity from recent list
2020-08-06 20:39:08 +08:00
2dust 9d58edd31f Merge pull request #537 from yuhan6665/revert
Revert "Refresh prepared domain every 30 minutes"
2020-08-06 20:38:50 +08:00
yuhan6665 af7dfc3a43 Revert "Refresh prepared domain every 30 minutes"
This reverts commit 903352ec9c.
2020-08-05 20:22:01 -04:00
yuhan6665 ca554e6ac4 Fix notification for Emui 4.1 2020-08-02 08:42:04 -04:00
xuezhixin ec391d8689 exclude ScSwitchActivity from recent list 2020-07-29 16:47:40 +08:00
2dust 6afd4d0549 Update AngConfigManager.kt 2020-07-27 21:00:34 +08:00
2dust 957cf85362 Merge pull request #517 from yuhan6665/bypass-private
Bypass private IP at VPN service
2020-07-25 19:42:12 +08:00
2dust 881152e10a Merge pull request #516 from cpdyj/master
add support to new version vmess link
2020-07-25 19:41:46 +08:00
2dust 6e47ebb27a Merge pull request #484 from DevRyz3n/drag-item-store-config
Optimize invoke timing of store config file after dragging item.
2020-07-25 19:41:13 +08:00
2dust a731e6c360 Merge pull request #453 from yuhan6665/widget
Widget
2020-07-25 19:40:39 +08:00
yuhan6665 d1466ba4b2 Bypass private IP at VPN service 2020-07-24 18:12:20 -04:00
iseki 617f28f399 Update AngConfigManager.kt 2020-07-24 11:23:46 +08:00
iseki c0ec0d9404 Update AngConfigManager.kt 2020-07-24 11:21:20 +08:00
iseki 3419dc8837 add support to new version vmess link
some information can be found at: https://github.com/v2ray/discussion/issues/720
2020-07-23 00:51:08 +08:00
2dust 786aaf823a Merge pull request #511 from yuhan6665/outbound-speed
Outbound speed
2020-07-19 08:19:03 +08:00
yuhan6665 1144183da4 Update notification icon based on traffic
Notification icon would be a good indication for current traffic
- going through proxy
- connect directly
- not going through v2rayNG
2020-07-18 18:48:47 -04:00
yuhan6665 5bf2fda990 Measure traffic from outbounds
With v2ray-core 4.26.0, traffic can be measured from outbounds.
Stats is shown separately for proxy and direct traffic.

In the future, it is possible to add stats for each server node and
even historic usage graph.
2020-07-18 18:47:59 -04:00
2dust 28639cc388 Merge pull request #468 from yuhan6665/cleanup-config
Remove legacy "connectionReuse" in config
2020-07-18 14:01:38 +08:00
2dust ca254b2aa1 Merge pull request #443 from yuhan6665/resume-speed
Fix speed display after screen turns on
2020-07-18 14:00:48 +08:00
2dust 9721879713 Merge pull request #432 from yuhan6665/drawer-ui
fix highlight issue in drawer ui
2020-07-18 13:59:47 +08:00
2dust 623c1807c5 Merge pull request #409 from yuhan6665/test-cleanup
Tcp test cleanup
2020-07-18 13:58:37 +08:00
2dust 739fa88ba7 Merge pull request #335 from yuhan6665/sub-index
Fix selected index when update subscription
2020-07-18 13:58:11 +08:00
2dust 85d6f00f8c Merge pull request #474 from yuhan6665/dns-cache-timeout
Refresh prepared domain every 30 minutes
2020-07-18 09:48:16 +08:00
yuhan6665 8986710453 Cancel async block when user test tcping again
Change async code to use Kotlin coroutine instead of Anko,
since Anko is deprecated and coroutine is the recommanded
approach now.
2020-07-16 19:42:43 -04:00
yuhan6665 f54faacbf6 Close connecting sockets when user test tcping again 2020-07-16 19:42:43 -04:00
r23 993ee0b8d2 Optimize invoke timing of store config file after dragging item. 2020-06-29 01:24:19 +08:00
yuhan6665 903352ec9c Refresh prepared domain every 30 minutes 2020-06-19 23:42:02 -04:00
yuhan6665 ad56106c08 Remove legacy "connectionReuse" in config
Also auto-refactor the data class naming to be proper camelcase
2020-06-13 23:38:33 -04:00
yuhan6665 91b8284afd Improve widget UI
Change widget color when clicked
2020-06-06 00:18:19 -04:00
yuhan6665 ef9e0cc0d2 Add back widget, fix implicit Intent 2020-06-06 00:18:19 -04:00
2dust 6577c46a31 Merge pull request #425 from yuhan6665/translation
Fix a traditional Chinese translation
2020-05-31 16:13:35 +08:00
yuhan6665 c6dab001b2 Fix traditional Chinese translation 2020-05-30 12:34:29 -04:00
yuhan6665 aea8369b8a Fix speed display after screen turns on
Also, use the tab to make the text less jumpy
2020-05-29 22:54:55 -04:00
yuhan6665 92d2cb35c4 Make drawer item unchecked
Current checked item is not consistent with the active activity.
In fact, we don't need checked state. This is a standard behavior.
You can find in apps like Google Playstore.
2020-05-24 11:13:12 -04:00
yuhan6665 6ce3d540e8 Remove config item in drawer
The drawer is attached to MainActivity. When user see the drawer, the
MainActivity must be active. Showing an menu item to launch itself is
confusing.
2020-05-24 11:09:11 -04:00
2dust c105d84b35 Merge pull request #300 from yuhan6665/master
Fix UI touch issues for Android TV
2020-04-12 10:04:52 +08:00
yuhan6665 8b149fb52f Fix selected index when update subscription 2020-04-03 18:58:41 -04:00
2dust 3ea04c076c Merge pull request #314 from yuhan6665/chrome-os
Add tags in AndroidManifest to support PlayStore filtering
2020-03-16 16:24:58 +08:00
2dust 98475460bf Merge pull request #313 from yuhan6665/lint-fix
Use annotation to fix lint error
2020-03-16 16:24:08 +08:00
yuhan6665 68ee61a753 Add tags in AndroidManifest to support PlayStore filtering
Google Playstore is using uses-feature and supports-screens
to filter devices.
By setting all screens to true and specifing features as
"not required" should make app compatible with more devices
Particularly it includes Chrome OS devices.
2020-03-14 14:07:31 -04:00
yuhan6665 90ba9ef2b7 Use annotation to fix lint error 2020-03-14 13:03:51 -04:00
yuhan6665 0ec114322e Fix UI touch issues for Android TV 2020-03-08 12:06:44 -04:00
2dust 76f52e7aa7 Merge pull request #287 from yuhan6665/multidex
Fix multidex for Android devices below L
2020-03-03 14:52:19 +08:00
2dust caa25ce424 Merge pull request #278 from yuhan6665/master
Add delete log button in logcat
2020-03-03 14:52:08 +08:00
yuhan6665 e33d7e9bcf Fix multidex for Android devices below L 2020-02-29 10:51:58 -05:00
yuhan6665 e120fce0b7 Add delete log button in logcat
Also, automatically scroll to bottom of the log if it is too long
2020-02-22 12:01:16 -05:00
2dust 6a90965c7b Update README.md 2019-11-14 13:09:31 +08:00
2dust 0542d913f2 Merge pull request #131 from galvasis193/feature-customediting
Give user the ability to edit/copy/paste custom config for convenience. Fix Import
2019-10-14 08:22:39 +08:00
Annoymous contributor 746254bb7e Give user the ability to edit/copy/paste custom config for convenience.
Fix import for go.Seq
2019-10-13 04:55:00 +00:00
2dust 2227ea3ed6 Merge pull request #129 from galvasis193/feature-ProxySharing
Add proxy sharing function. Fix potential security vulnerbility.
2019-10-12 09:40:19 +08:00
Annoymous contributor 4d0e9e2b44 Bugfix: When a setting is changed proxy will now auto restart to apply the change. Fix proxy service cannot be stopped bug. 2019-10-11 10:28:45 +00:00
Annoymous contributor 5cba8a228c Add proxy sharing. Fix potential unahthorized access from LAN/Internet. 2019-10-11 10:26:46 +00:00
2dust 2d8ed22dba mge 2019-10-11 14:17:10 +08:00
285 changed files with 5072 additions and 11697 deletions
-63
View File
@@ -1,63 +0,0 @@
name: Build APK
on:
push:
workflow_dispatch:
inputs:
XRAY_CORE_VERSION:
description: 'Xray core version or commit hash'
required: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Golang
uses: actions/setup-go@v5
with:
go-version: '1.22.2'
- name: Install gomobile
run: |
go install golang.org/x/mobile/cmd/gomobile@latest
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Setup Android environment
uses: android-actions/setup-android@v3
- name: Build dependencies
run: |
mkdir ${{ github.workspace }}/build
cd ${{ github.workspace }}/build
git clone --depth=1 -b main https://github.com/2dust/AndroidLibXrayLite.git
cd AndroidLibXrayLite
go get github.com/xtls/xray-core@${{ github.event.inputs.XRAY_CORE_VERSION }} || true
gomobile init
go mod tidy -v
gomobile bind -v -androidapi 21 -ldflags='-s -w' ./
cp *.aar ${{ github.workspace }}/V2rayNG/app/libs/
- name: Build APK
run: |
cd ${{ github.workspace }}/V2rayNG
chmod 755 gradlew
./gradlew assembleDebug
- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: apk
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/
+15 -26
View File
@@ -1,40 +1,29 @@
**v2rayNG 隐私权政策** v2rayNG 隐私条款
本政策自2023年11月17日起施行 最后更新 2017-11-22
2dust 将 v2rayNG 应用程序构建为开源应用程序。 本服务由 2dust 免费提供,并且旨在按原样使用。 v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私条款。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私权政策。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。** 1. 信息收集
**信息收集** v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私条款。
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私权政策。 v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。 当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。** 2. 信息共享
**信息共享** 我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。 3. 信息存留
**信息存留** 除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。 4. 信息泄露
**信息泄露** 我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。** 5. 条款修改
**儿童隐私**
这些服务不针对 13 岁以下的任何人。我不会故意收集 13 岁以下儿童的个人身份信息。 如果我发现 13 岁以下的儿童向我提供了个人信息,我会立即从我们的服务器中删除该信息。 如果您是父母或监护人,并且您知道您的孩子向我们提供了个人信息,请与我联系,以便我能够采取必要的行动。
**条款修改**
我们保留修改这份隐私权政策的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私权政策。**
**联系我们**
如果您对我的隐私政策有任何疑问或建议,请随时通过 CaptainIronng@protonmail.com 与我联系。
我们保留修改这份隐私条款的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。**
+1 -1
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.9.23-blue.svg)](https://kotlinlang.org) [![Kotlin Version](https://img.shields.io/badge/Kotlin-1.6.21-blue.svg)](https://kotlinlang.org)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayNG)](https://github.com/2dust/v2rayNG/commits/master) [![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)
+151
View File
@@ -0,0 +1,151 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
Properties props = new Properties()
props.load(new FileInputStream(new File('local.properties')))
android {
compileSdkVersion Integer.parseInt("$compileSdkVer")
buildToolsVersion "$buildToolsVer"
compileOptions {
targetCompatibility = "8"
sourceCompatibility = "8"
}
defaultConfig {
applicationId "com.v2ray.ang"
minSdkVersion 21
targetSdkVersion Integer.parseInt("$targetSdkVer")
multiDexEnabled true
versionCode 476
versionName "1.7.21"
}
if (props["sign"]) {
signingConfigs {
release {
storeFile file("../key.jks")
keyAlias 'ang'
keyPassword '123456'
storePassword '123456'
}
debug {
storeFile file("../key.jks")
keyAlias 'ang'
keyPassword '123456'
storePassword '123456'
}
}
}
buildTypes {
release {
minifyEnabled false
zipAlignEnabled false
shrinkResources false
if (props["sign"]) {
signingConfig signingConfigs.release
}
ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
zipAlignEnabled false
shrinkResources false
if (props["sign"]) {
signingConfig signingConfigs.release
}
ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
java.srcDirs += 'src/main/kotlin'
}
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
splits {
abi {
enable true
reset()
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
universalApk true //generate an additional APK that contains all the ABIs
}
}
// map for the version code
project.ext.versionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
android.applicationVariants.all { variant ->
// assign different version code for each output
variant.outputs.each { output ->
output.outputFileName = "v2rayNG_" + variant.versionName + "_" + output.getFilter(com.android.build.OutputFile.ABI) + ".apk"
output.versionCodeOverride =
project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) *
1000000 + android.defaultConfig.versionCode
}
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
testImplementation 'junit:junit:4.13.2'
// Androidx
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.fragment:fragment-ktx:1.5.2'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
// Androidx ktx
implementation 'androidx.activity:activity-ktx:1.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
//kotlin
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
implementation 'com.tencent:mmkv-static:1.2.12'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'io.reactivex:rxjava:1.3.4'
implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'
implementation 'me.dm7.barcodescanner:core:1.9.8'
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01@aar'
implementation 'me.drakeet.support:toastcompat:1.1.0'
implementation 'com.blacksquircle.ui:editorkit:2.1.1'
implementation 'com.blacksquircle.ui:language-base:2.1.1'
implementation 'com.blacksquircle.ui:language-json:2.1.1'
}
//buildscript {
// repositories {
// google()
// mavenCentral()
// maven { url 'https://maven.google.com' }
// maven { url 'https://jitpack.io' }
// }
//}
-132
View File
@@ -1,132 +0,0 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.v2ray.ang"
compileSdk = 34
defaultConfig {
applicationId = "com.v2ray.ang"
minSdk = 21
targetSdk = 34
versionCode = 602
versionName = "1.9.8"
multiDexEnabled = true
splits {
abi {
isEnable = true
include(
"arm64-v8a",
"armeabi-v7a",
"x86_64",
"x86"
)
isUniversalApk = true
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
buildTypes {
release {
isMinifyEnabled = false
}
debug {
isMinifyEnabled = false
}
}
sourceSets {
getByName("main") {
jniLibs.srcDirs("libs")
}
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
applicationVariants.all {
val variant = this
val versionCodes =
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4)
variant.outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output ->
val abi = if (output.getFilter("ABI") != null)
output.getFilter("ABI")
else
"universal"
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
if (versionCodes.containsKey(abi)) {
output.versionCodeOverride = (1000000 * versionCodes[abi]!!).plus(variant.versionCode)
} else {
return@forEach
}
}
}
buildFeatures {
viewBinding = true
buildConfig = true
}
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
testImplementation(libs.junit)
implementation(libs.flexbox)
// Androidx
implementation(libs.constraintlayout)
implementation(libs.legacy.support.v4)
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.cardview)
implementation(libs.preference.ktx)
implementation(libs.recyclerview)
implementation(libs.fragment.ktx)
implementation(libs.multidex)
implementation(libs.viewpager2)
// Androidx ktx
implementation(libs.activity.ktx)
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.lifecycle.livedata.ktx)
implementation(libs.lifecycle.runtime.ktx)
//kotlin
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.mmkv.static)
implementation(libs.gson)
implementation(libs.rxjava)
implementation(libs.rxandroid)
implementation(libs.rxpermissions)
implementation(libs.toastcompat)
implementation(libs.editorkit)
implementation(libs.language.base)
implementation(libs.language.json)
implementation(libs.quickie.bundled)
implementation(libs.core)
implementation(libs.work.runtime.ktx)
implementation(libs.work.multiprocess)
}
Binary file not shown.
@@ -0,0 +1,13 @@
package com.v2ray.ang;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="app_name" type="string">v2rayNG (DEV)</item>
</resources>
+61 -142
View File
@@ -1,36 +1,21 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:ignore="MissingLeanbackLauncher"> package="com.v2ray.ang">
<supports-screens <supports-screens
android:anyDensity="true" android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true" android:smallScreens="true"
android:xlargeScreens="true" /> android:normalScreens="true"
android:largeScreens="true"
android:xlargeScreens="true"/>
<uses-sdk <uses-feature android:name="android.hardware.camera" android:required="false"/>
android:minSdkVersion="21" <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
tools:overrideLibrary="com.blacksquircle.ui.editorkit" />
<uses-feature
android:name="android.hardware.camera"
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 <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
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" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@@ -38,121 +23,97 @@
<!-- <useapplications-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> --> <!-- <useapplications-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
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.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application <application
android:name=".AngApplication" android:name=".AngApplication"
android:allowBackup="true" android:allowBackup="true"
android:banner="@mipmap/ic_banner"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppThemeDayNight" android:theme="@style/AppThemeLight"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="m"> tools:targetApi="m">
<activity <activity
android:name=".ui.MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTask" android:name=".ui.MainActivity"
android:theme="@style/AppThemeDayNight.NoActionBar"> android:launchMode="singleTask">
<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" />
</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" />
</activity> </activity>
<activity <activity
android:exported="false"
android:name=".ui.ServerActivity" android:name=".ui.ServerActivity"
android:exported="false"
android:windowSoftInputMode="stateUnchanged" /> android:windowSoftInputMode="stateUnchanged" />
<activity <activity
android:exported="false"
android:name=".ui.ServerCustomConfigActivity" android:name=".ui.ServerCustomConfigActivity"
android:exported="false"
android:windowSoftInputMode="stateUnchanged" /> android:windowSoftInputMode="stateUnchanged" />
<activity <activity
android:name=".ui.SettingsActivity" android:exported="false"
android:exported="false" /> android:name=".ui.SettingsActivity" />
<activity <activity
android:name=".ui.PerAppProxyActivity" android:exported="false"
android:exported="false" /> android:name=".ui.PerAppProxyActivity" />
<activity <activity
android:name=".ui.ScannerActivity" android:exported="false"
android:exported="false" /> android:name=".ui.ScannerActivity" />
<activity <activity
android:name=".ui.LogcatActivity" android:exported="false"
android:exported="false" /> android:name=".ui.LogcatActivity" />
<activity <activity
android:name=".ui.RoutingSettingActivity" android:exported="false"
android:exported="false" /> android:name=".ui.RoutingSettingsActivity"
android:windowSoftInputMode="stateUnchanged" />
<activity <activity
android:name=".ui.RoutingEditActivity" android:exported="false"
android:exported="false" /> android:name=".ui.SubSettingActivity" />
<activity <activity
android:name=".ui.SubSettingActivity" android:exported="false"
android:exported="false" /> android:name=".ui.UserAssetActivity" />
<activity
android:name=".ui.UserAssetActivity"
android:exported="false" />
<activity
android:name=".ui.UserAssetUrlActivity"
android:exported="false" />
<activity <activity
android:name=".ui.SubEditActivity" android:exported="false"
android:exported="false" /> android:name=".ui.SubEditActivity" />
<activity <activity
android:name=".ui.ScScannerActivity" android:exported="false"
android:exported="false" /> android:name=".ui.ScScannerActivity" />
<activity <activity
android:exported="false"
android:name=".ui.ScSwitchActivity" android:name=".ui.ScSwitchActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="false"
android:process=":RunSoLibV2RayDaemon" android:process=":RunSoLibV2RayDaemon"
android:theme="@style/AppTheme.NoActionBar.Translucent" /> android:theme="@style/AppTheme.NoActionBar.Translucent" />
<activity <activity
android:name=".ui.UrlSchemeActivity" android:exported="true"
android:exported="true"> android:name=".ui.UrlSchemeActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<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.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.DEFAULT" /> <data android:scheme="v2rayng"
android:host="install-config" />
<data android:scheme="v2rayng" />
<data android:host="install-config" />
<data android:host="install-sub" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.AboutActivity"
android:exported="false" />
<service <service
android:name=".service.V2RayVpnService" android:name=".service.V2RayVpnService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="specialUse"
android:label="@string/app_name" android:label="@string/app_name"
android:permission="android.permission.BIND_VPN_SERVICE" android:permission="android.permission.BIND_VPN_SERVICE"
android:process=":RunSoLibV2RayDaemon"> android:process=":RunSoLibV2RayDaemon">
@@ -162,68 +123,48 @@
<meta-data <meta-data
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON" android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="true" /> android:value="true" />
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="vpn" />
</service> </service>
<service <service android:name=".service.V2RayProxyOnlyService"
android:name=".service.V2RayProxyOnlyService" android:exported="false"
android:label="@string/app_name"
android:process=":RunSoLibV2RayDaemon">
</service>
<service android:name=".service.V2RayTestService"
android:exported="false" android:exported="false"
android:foregroundServiceType="specialUse"
android:label="@string/app_name"
android:process=":RunSoLibV2RayDaemon"> android:process=":RunSoLibV2RayDaemon">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="proxy" />
</service> </service>
<service
android:name=".service.V2RayTestService"
android:exported="false"
android:process=":RunSoLibV2RayDaemon" />
<receiver <receiver
android:name=".receiver.WidgetProvider" android:exported="true"
android:exported="true" android:name=".receiver.WidgetProvider"
android:process=":RunSoLibV2RayDaemon"> android:process=":RunSoLibV2RayDaemon">
<meta-data <meta-data
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/app_widget_provider" /> android:resource="@xml/app_widget_provider" />
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="com.v2ray.ang.action.widget.click" /> <action android:name="com.v2ray.ang.action.widget.click" />
<action android:name="com.v2ray.ang.action.activity" /> <action android:name="com.v2ray.ang.action.activity" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver
android:name=".receiver.BootReceiver"
android:exported="true"
android:label="BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service <service
android:name=".service.QSTileService" android:exported="true"
android:exported="true" android:name=".service.QSTileService"
android:foregroundServiceType="specialUse" android:icon="@drawable/ic_stat_name"
android:icon="@drawable/ic_stat_name" android:label="@string/app_tile_name"
android:label="@string/app_tile_name" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" android:process=":RunSoLibV2RayDaemon">
android:process=":RunSoLibV2RayDaemon">
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="tile" />
</service> </service>
<!-- =====================Tasker===================== --> <!-- =====================Tasker===================== -->
<activity <activity
android:name=".ui.TaskerActivity"
android:exported="true" android:exported="true"
android:name=".ui.TaskerActivity"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"> android:label="@string/app_name">
<intent-filter> <intent-filter>
@@ -232,36 +173,14 @@
</activity> </activity>
<receiver <receiver
android:name=".receiver.TaskerReceiver"
android:exported="true" android:exported="true"
android:process=":RunSoLibV2RayDaemon"> android:name=".receiver.TaskerReceiver"
android:process=":RunSoLibV2RayDaemon">
<intent-filter> <intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" /> <action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- =====================Tasker===================== --> <!-- =====================Tasker===================== -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</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>
@@ -1,73 +0,0 @@
[
{
"remarks": "绕过bittorrent",
"outboundTag": "direct",
"protocol": [
"bittorrent"
]
},
{
"remarks": "Google cn",
"outboundTag": "proxy",
"domain": [
"domain:googleapis.cn",
"domain:gstatic.com"
]
},
{
"remarks": "阻断udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",
"ip": [
"geoip:private"
]
},
{
"remarks": "绕过局域网域名",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "代理GFW",
"outboundTag": "proxy",
"domain": [
"geosite:gfw",
"geosite:greatfire"
]
},
{
"remarks": "代理Google等",
"outboundTag": "proxy",
"ip": [
"1.0.0.1",
"1.1.1.1",
"8.8.8.8",
"8.8.4.4",
"geoip:facebook",
"geoip:fastly",
"geoip:google",
"geoip:netflix",
"geoip:telegram",
"geoip:twitter"
]
},
{
"remarks": "最终直连",
"port": "0-65535",
"outboundTag": "direct"
}
]
@@ -0,0 +1 @@
geosite:category-ads-all,
@@ -0,0 +1,132 @@
domain:12306.com,
domain:51ym.me,
domain:52pojie.cn,
domain:8686c.com,
domain:abercrombie.com,
domain:adobesc.com,
domain:air-matters.com,
domain:air-matters.io,
domain:airtable.com,
domain:akadns.net,
domain:apache.org,
domain:api.crisp.chat,
domain:api.termius.com,
domain:appshike.com,
domain:appstore.com,
domain:aweme.snssdk.com,
domain:bababian.com,
domain:battle.net,
domain:beatsbydre.com,
domain:bet365.com,
domain:bilibili.cn,
domain:ccgslb.com,
domain:ccgslb.net,
domain:chunbo.com,
domain:chunboimg.com,
domain:clashroyaleapp.com,
domain:cloudsigma.com,
domain:cloudxns.net,
domain:cmfu.com,
domain:culturedcode.com,
domain:dct-cloud.com,
domain:didialift.com,
domain:douyutv.com,
domain:duokan.com,
domain:dytt8.net,
domain:easou.com,
domain:ecitic.net,
domain:eclipse.org,
domain:eudic.net,
domain:ewqcxz.com,
domain:fir.im,
domain:frdic.com,
domain:fresh-ideas.cc,
domain:godic.net,
domain:goodread.com,
domain:haibian.com,
domain:hdslb.net,
domain:hollisterco.com,
domain:hongxiu.com,
domain:hxcdn.net,
domain:images.unsplash.com,
domain:img4me.com,
domain:ipify.org,
domain:ixdzs.com,
domain:jd.hk,
domain:jianshuapi.com,
domain:jomodns.com,
domain:jsboxbbs.com,
domain:knewone.com,
domain:kuaidi100.com,
domain:lemicp.com,
domain:letvcloud.com,
domain:lizhi.io,
domain:localizecdn.com,
domain:lucifr.com,
domain:luoo.net,
domain:mai.tn,
domain:maven.org,
domain:miwifi.com,
domain:moji.com,
domain:moke.com,
domain:mtalk.google.com,
domain:mxhichina.com,
domain:myqcloud.com,
domain:myunlu.com,
domain:netease.com,
domain:nfoservers.com,
domain:nssurge.com,
domain:nuomi.com,
domain:ourdvs.com,
domain:overcast.fm,
domain:paypal.com,
domain:paypalobjects.com,
domain:pgyer.com,
domain:qdaily.com,
domain:qdmm.com,
domain:qin.io,
domain:qingmang.me,
domain:qingmang.mobi,
domain:qqurl.com,
domain:rarbg.to,
domain:rrmj.tv,
domain:ruguoapp.com,
domain:sm.ms,
domain:snwx.com,
domain:soku.com,
domain:startssl.com,
domain:store.steampowered.com,
domain:symcd.com,
domain:teamviewer.com,
domain:tmzvps.com,
domain:trello.com,
domain:trellocdn.com,
domain:ttmeiju.com,
domain:udache.com,
domain:uxengine.net,
domain:weather.bjango.com,
domain:weather.com,
domain:webqxs.com,
domain:weico.cc,
domain:wenku8.net,
domain:werewolf.53site.com,
domain:windowsupdate.com,
domain:wkcdn.com,
domain:workflowy.com,
domain:xdrig.com,
domain:xiaojukeji.com,
domain:xiaomi.net,
domain:xiaomicp.com,
domain:ximalaya.com,
domain:xitek.com,
domain:xmcdn.com,
domain:xslb.net,
domain:xteko.com,
domain:yach.me,
domain:yixia.com,
domain:yunjiasu-cdn.net,
domain:zealer.com,
domain:zgslb.net,
domain:zimuzu.tv,
domain:zmz002.com,
domain:samsungdm.com,
@@ -1,34 +0,0 @@
[
{
"remarks": "阻断udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",
"ip": [
"geoip:private"
]
},
{
"remarks": "绕过局域网域名",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "最终代理",
"port": "0-65535",
"outboundTag": "proxy"
}
]
@@ -0,0 +1,33 @@
geosite:google,
geosite:github,
geosite:netflix,
geosite:steam,
geosite:telegram,
geosite:tumblr,
geosite:speedtest,
geosite:bbc,
domain:gvt1.com,
domain:textnow.com,
domain:twitch.tv,
domain:wikileaks.org,
domain:naver.com,
91.108.4.0/22,
91.108.8.0/22,
91.108.12.0/22,
91.108.20.0/22,
91.108.36.0/23,
91.108.38.0/23,
91.108.56.0/22,
149.154.160.0/20,
149.154.164.0/22,
149.154.172.0/22,
74.125.0.0/16,
173.194.0.0/16,
172.217.0.0/16,
216.58.200.0/24,
216.58.220.0/24,
91.108.56.116,
91.108.56.0/24,
109.239.140.0/24,
149.154.167.0/24,
149.154.175.0/24,
@@ -1,57 +0,0 @@
[
{
"remarks": "阻断udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",
"ip": [
"geoip:private"
]
},
{
"remarks": "绕过局域网域名",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "绕过中国IP",
"outboundTag": "direct",
"ip": [
"geoip:cn"
]
},
{
"remarks": "绕过中国域名",
"outboundTag": "direct",
"domain": [
"geosite:cn",
"geosite:geolocation-cn"
]
},
{
"remarks": "Google CN",
"outboundTag": "proxy",
"domain": [
"domain:googleapis.cn",
"domain:gstatic.com"
]
},
{
"remarks": "最终代理",
"port": "0-65535",
"outboundTag": "proxy"
}
]
@@ -1,49 +0,0 @@
[
{
"remarks": "Block udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "Block ads and trackers",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "Direct LAN IP",
"outboundTag": "direct",
"ip": [
"geoip:private"
]
},
{
"remarks": "Direct LAN domains",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "Bypass Iran domains",
"outboundTag": "direct",
"domain": [
"domain:ir",
"geosite:category-ir"
]
},
{
"remarks": "Bypass Iran IP",
"outboundTag": "direct",
"ip": [
"geoip:ir"
]
},
{
"remarks": "Final Agent",
"port": "0-65535",
"outboundTag": "proxy"
}
]
@@ -81,9 +81,7 @@
}, },
{ {
"protocol": "freedom", "protocol": "freedom",
"settings": { "settings": {},
"domainStrategy": "UseIP"
},
"tag": "direct" "tag": "direct"
}, },
{ {
@@ -16,8 +16,8 @@
package com.v2ray.ang.helper; package com.v2ray.ang.helper;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
/** /**
* 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,6 +36,7 @@ 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()
*/ */
@@ -51,6 +52,7 @@ 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()
*/ */
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2015 Paul Burke
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.v2ray.ang.helper;
import androidx.recyclerview.widget.RecyclerView;
/**
* Listener for manual initiation of a drag.
*/
public interface OnStartDragListener {
/**
* Called when a view is requesting a start of a drag.
*
* @param viewHolder The holder of the view to drag.
*/
void onStartDrag(RecyclerView.ViewHolder viewHolder);
}
@@ -17,10 +17,9 @@
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.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -1,52 +1,28 @@
package com.v2ray.ang package com.v2ray.ang
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import androidx.work.Configuration import androidx.preference.PreferenceManager
import androidx.work.WorkManager
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.util.SettingsManager
import com.v2ray.ang.util.Utils
class AngApplication : MultiDexApplication() { 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
} }
override fun attachBaseContext(base: Context?) { var firstRun = false
super.attachBaseContext(base) private set
application = this
}
private val workManagerConfiguration: Configuration = Configuration.Builder()
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
.build()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// LeakCanary.install(this) // LeakCanary.install(this)
// val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
// firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
// if (firstRun) if (firstRun)
// defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply() defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
//Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE)
MMKV.initialize(this) MMKV.initialize(this)
Utils.setNightMode(application)
// Initialize WorkManager with the custom configuration
WorkManager.initialize(this, workManagerConfiguration)
SettingsManager.initRoutingRulesets(this)
} }
fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(
packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES
)!!
} }
@@ -1,125 +1,76 @@
package com.v2ray.ang package com.v2ray.ang
/**
*
* App Config Const
*/
object AppConfig { object AppConfig {
const val ANG_PACKAGE = "com.v2ray.ang"
/** The application's package name. */
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"
/** Legacy configuration keys. */ // legacy
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"
const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
/** Preferences mapped to MMKV storage. */ // Preferences mapped to MMKV
const val PREF_MODE = "pref_mode"
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled" const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
const val PREF_ROUTE_ONLY_ENABLED = "pref_route_only_enabled" const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
const val PREF_BYPASS_APPS = "pref_bypass_apps"
const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled" const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
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_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_REMOTE_DNS = "pref_remote_dns"
const val PREF_ROUTING_RULESET = "pref_routing_ruleset" const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
const val PREF_MUX_ENABLED = "pref_mux_enabled" const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
const val PREF_MUX_CONCURRENCY = "pref_mux_concurrency"
const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurrency"
const val PREF_MUX_XUDP_QUIC = "pref_mux_xudp_quic"
const val PREF_FRAGMENT_ENABLED = "pref_fragment_enabled"
const val PREF_FRAGMENT_PACKETS = "pref_fragment_packets"
const val PREF_FRAGMENT_LENGTH = "pref_fragment_length"
const val PREF_FRAGMENT_INTERVAL = "pref_fragment_interval"
const val SUBSCRIPTION_AUTO_UPDATE = "pref_auto_update_subscription"
const val SUBSCRIPTION_AUTO_UPDATE_INTERVAL = "pref_auto_update_interval"
const val SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL = "1440" // Default is 24 hours
const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater"
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
const val PREF_LANGUAGE = "pref_language"
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
const val PREF_ALLOW_INSECURE = "pref_allow_insecure" const val PREF_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_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_LANGUAGE = "pref_language"
const val PREF_IS_BOOTED = "pref_is_booted" const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
const val PREF_ROUTING_MODE = "pref_routing_mode"
const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent"
const val PREF_V2RAY_ROUTING_DIRECT = "pref_v2ray_routing_direct"
const val PREF_V2RAY_ROUTING_BLOCKED = "pref_v2ray_routing_blocked"
const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
const val PREF_BYPASS_APPS = "pref_bypass_apps"
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
/** Cache keys. */ const val HTTP_PROTOCOL: String = "http://"
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id" const val HTTPS_PROTOCOL: String = "https://"
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
/** Protocol identifiers. */
const val PROTOCOL_HTTP: String = "http://"
const val PROTOCOL_HTTPS: String = "https://"
const val PROTOCOL_FREEDOM: String = "freedom"
/** Broadcast actions. */
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_AGENT = "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"
/** Network-related constants. */ const val androidpackagenamelistUrl = "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
const val UPLINK = "uplink" const val v2rayCustomRoutingListUrl = "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
const val DOWNLINK = "downlink" const val v2rayNGIssues = "https://github.com/2dust/v2rayNG/issues"
const val v2rayNGWikiMode = "https://github.com/2dust/v2rayNG/wiki/Mode"
const val promotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
const val geoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/";
/** URLs for various resources. */ const val DNS_AGENT = "1.1.1.1"
const val androidpackagenamelistUrl =
"https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
const val v2rayCustomRoutingListUrl =
"https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
const val v2rayNGUrl = "https://github.com/2dust/v2rayNG"
const val v2rayNGIssues = "$v2rayNGUrl/issues"
const val v2rayNGWikiMode = "$v2rayNGUrl/wiki/Mode"
const val v2rayNGPrivacyPolicy = "https://raw.githubusercontent.com/2dust/v2rayNG/master/CR.md"
const val PromotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/"
const val TgChannelUrl = "https://t.me/github_2dust"
const val DelayTestUrl = "https://www.gstatic.com/generate_204"
const val DelayTestUrl2 = "https://www.google.com/generate_204"
/** DNS server addresses. */
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 GEOSITE_PRIVATE = "geosite:private"
const val GEOSITE_CN = "geosite:cn"
const val GEOIP_PRIVATE = "geoip:private"
const val GEOIP_CN = "geoip:cn"
/** Ports and addresses for various services. */
const val PORT_LOCAL_DNS = "10853" const val PORT_LOCAL_DNS = "10853"
const val PORT_SOCKS = "10808" const val PORT_SOCKS = "10808"
const val PORT_HTTP = "10809" const val PORT_HTTP = "10809"
const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32"
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
const val WIREGUARD_LOCAL_MTU = "1420"
const val LOOPBACK = "127.0.0.1"
/** 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
@@ -135,27 +86,4 @@ 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 HTTP = "http://"
const val VLESS = "vless://"
const val TROJAN = "trojan://"
const val WIREGUARD = "wireguard://"
const val TUIC = "tuic://"
const val HYSTERIA2 = "hysteria2://"
const val HY2 = "hy2://"
/** Give a good name to this, IDK*/
const val VPN = "VPN"
} }
@@ -0,0 +1,32 @@
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)
}
@@ -2,10 +2,8 @@ package com.v2ray.ang.dto
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
data class AppInfo( data class AppInfo(val appName: String,
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
)
@@ -1,8 +0,0 @@
package com.v2ray.ang.dto
data class AssetUrlItem(
var remarks: String = "",
var url: String = "",
val addedTime: Long = System.currentTimeMillis(),
var lastUpdated: Long = -1
)
@@ -1,9 +0,0 @@
package com.v2ray.ang.dto
data class ConfigResult (
var status: Boolean,
var guid: String? = null,
var content: String = "",
var domainPort: String? = null,
)
@@ -1,19 +1,12 @@
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, AppConfig.VMESS), VMESS(1, "vmess://"),
CUSTOM(2, AppConfig.CUSTOM), CUSTOM(2, ""),
SHADOWSOCKS(3, AppConfig.SHADOWSOCKS), SHADOWSOCKS(3, "ss://"),
SOCKS(4, AppConfig.SOCKS), SOCKS(4, "socks://"),
VLESS(5, AppConfig.VLESS), VLESS(5, "vless://"),
TROJAN(6, AppConfig.TROJAN), TROJAN(6, "trojan://");
WIREGUARD(7, AppConfig.WIREGUARD),
// TUIC(8, AppConfig.TUIC),
HYSTERIA2(9, AppConfig.HYSTERIA2),
HTTP(10, AppConfig.HTTP);
companion object { companion object {
fun fromInt(value: Int) = values().firstOrNull { it.value == value } fun fromInt(value: Int) = values().firstOrNull { it.value == value }
@@ -0,0 +1,9 @@
package com.v2ray.ang.dto
enum class ERoutingMode(val value: String ) {
GLOBAL_PROXY("0"),
BYPASS_LAN("1"),
BYPASS_MAINLAND("2"),
BYPASS_LAN_MAINLAND("3"),
GLOBAL_DIRECT("4");
}
@@ -1,29 +0,0 @@
package com.v2ray.ang.dto
data class Hysteria2Bean(
val server: String?,
val auth: String?,
val lazy: Boolean? = true,
val obfs: ObfsBean? = null,
val socks5: Socks5Bean? = null,
val http: Socks5Bean? = null,
val tls: TlsBean? = null,
) {
data class ObfsBean(
val type: String?,
val salamander: SalamanderBean?
) {
data class SalamanderBean(
val password: String?,
)
}
data class Socks5Bean(
val listen: String?,
)
data class TlsBean(
val sni: String?,
val insecure: Boolean?,
)
}
@@ -1,9 +0,0 @@
package com.v2ray.ang.dto
data class ProfileItem(
val configType: EConfigType,
var subscriptionId: String = "",
var remarks: String = "",
var server: String?,
var serverPort: Int?,
)
@@ -1,13 +0,0 @@
package com.v2ray.ang.dto
data class RulesetItem(
var remarks: String? = "",
var ip: List<String>? = null,
var domain: List<String>? = null,
var outboundTag: String = "",
var port: String? = null,
var network: String? = null,
var protocol: List<String>? = null,
var enabled: Boolean = true,
var looked: Boolean? = false,
)
@@ -1,68 +1,41 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto
import com.v2ray.ang.AppConfig.TAG_AGENT
import com.v2ray.ang.AppConfig.TAG_BLOCKED import com.v2ray.ang.AppConfig.TAG_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
data class ServerConfig( data class ServerConfig(
val configVersion: Int = 3, val configVersion: Int = 3,
val configType: EConfigType, val configType: EConfigType,
var subscriptionId: String = "", var subscriptionId: String = "",
val addedTime: Long = System.currentTimeMillis(), val addedTime: Long = System.currentTimeMillis(),
var remarks: String = "", var remarks: String = "",
val outboundBean: V2rayConfig.OutboundBean? = null, val outboundBean: V2rayConfig.OutboundBean? = null,
var fullConfig: V2rayConfig? = null var fullConfig: V2rayConfig? = null
) { ) {
companion object { companion object {
fun create(configType: EConfigType): ServerConfig { fun create(configType: EConfigType): ServerConfig {
when (configType) { when(configType) {
EConfigType.VMESS, EConfigType.VMESS, EConfigType.VLESS ->
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( vnext = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
V2rayConfig.OutboundBean.OutSettingsBean.VnextBean( users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())))),
users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean()) streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
)
)
),
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.HTTP,
EConfigType.TROJAN,
EConfigType.HYSTERIA2 ->
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 ->
return ServerConfig(
configType = configType,
outboundBean = V2rayConfig.OutboundBean(
protocol = configType.name.lowercase(),
settings = V2rayConfig.OutboundBean.OutSettingsBean(
secretKey = "",
peers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean())
)
)
)
} }
} }
} }
@@ -76,11 +49,21 @@ data class ServerConfig(
fun getAllOutboundTags(): MutableList<String> { fun getAllOutboundTags(): MutableList<String> {
if (configType != EConfigType.CUSTOM) { if (configType != EConfigType.CUSTOM) {
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED) return mutableListOf(TAG_AGENT, TAG_DIRECT, TAG_BLOCKED)
} }
fullConfig?.let { config -> fullConfig?.let { config ->
return config.outbounds.map { it.tag }.toMutableList() return config.outbounds.map { it.tag }.toMutableList()
} }
return mutableListOf() return mutableListOf()
} }
fun getV2rayPointDomainAndPort(): String {
val address = getProxyOutbound()?.getServerAddress().orEmpty()
val port = getProxyOutbound()?.getServerPort()
return if (Utils.isIpv6Address(address)) {
String.format("[%s]:%s", address, port)
} else {
String.format("%s:%s", address, port)
}
}
} }
@@ -1,6 +1,4 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto
data class ServersCache( data class ServersCache(val guid: String,
val guid: String, val config: ServerConfig)
val profile: ProfileItem
)
@@ -1,15 +1,8 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto
data class SubscriptionItem( data class SubscriptionItem(
var remarks: String = "", var remarks: String = "",
var url: String = "", var url: String = "",
var enabled: Boolean = true, var enabled: Boolean = true,
val addedTime: Long = System.currentTimeMillis(), val addedTime: Long = System.currentTimeMillis()) {
var lastUpdated: Long = -1, }
var autoUpdate: Boolean = false,
val updateInterval: Int? = null,
var prevProfile: String? = null,
var nextProfile: String? = null,
var filter: String? = null,
)
@@ -7,311 +7,195 @@ import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer import com.google.gson.JsonSerializer
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.v2ray.ang.util.Utils
import java.lang.reflect.Type import java.lang.reflect.Type
data class V2rayConfig( data class V2rayConfig(
var remarks: String? = null, var stats: Any? = null,
var stats: Any? = null, val log: LogBean,
val log: LogBean, var policy: PolicyBean?,
var policy: PolicyBean?, val inbounds: ArrayList<InboundBean>,
val inbounds: ArrayList<InboundBean>, var outbounds: ArrayList<OutboundBean>,
var outbounds: ArrayList<OutboundBean>, var dns: DnsBean,
var dns: DnsBean, val routing: RoutingBean,
val routing: RoutingBean, val api: Any? = null,
val api: Any? = null, val transport: Any? = null,
val transport: Any? = null, val reverse: Any? = null,
val reverse: Any? = null, var fakedns: Any? = null,
var fakedns: Any? = null, val browserForwarder: Any? = null) {
val browserForwarder: Any? = null,
var observatory: 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"
const val DEFAULT_LEVEL = 8 const val DEFAULT_LEVEL = 8
const val DEFAULT_NETWORK = "tcp" const val DEFAULT_NETWORK = "tcp"
const val DEFAULT_FLOW = "xtls-rprx-splice"
const val TLS = "tls" const val TLS = "tls"
const val REALITY = "reality" const val XTLS = "xtls"
const val HTTP = "http" const val HTTP = "http"
} }
data class LogBean( data class LogBean(val access: String,
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,
var port: Int, var port: Int,
var protocol: String, var protocol: String,
var listen: String? = null, var listen: String? = null,
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( data class InSettingsBean(val auth: String? = null,
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( data class SniffingBean(var enabled: Boolean,
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( data class OutboundBean(val tag: String = "proxy",
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, val mux: MuxBean? = MuxBean(false)) {
var mux: MuxBean? = MuxBean(false)
) {
data class OutSettingsBean( data class OutSettingsBean(var vnext: List<VnextBean>? = null,
var vnext: List<VnextBean>? = null, var servers: List<ServersBean>? = null,
var fragment: FragmentBean? = null, /*Blackhole*/
var noises: List<NoiseBean>? = null, var response: Response? = null,
var servers: List<ServersBean>? = null, /*DNS*/
/*Blackhole*/ val network: String? = null,
var response: Response? = null, val address: String? = null,
/*DNS*/ val port: Int? = null,
val network: String? = null, /*Freedom*/
var address: Any? = null, var domainStrategy: String? = null,
val port: Int? = null, val redirect: String? = null,
/*Freedom*/ val userLevel: Int? = null,
var domainStrategy: String? = null, /*Loopback*/
val redirect: String? = null, val inboundTag: String? = null) {
val userLevel: Int? = null,
/*Loopback*/
val inboundTag: String? = null,
/*Wireguard*/
var secretKey: String? = null,
val peers: List<WireGuardBean>? = null,
var reserved: List<Int>? = null,
var mtu: Int? = null,
var obfsPassword: String? = null,
) {
data class VnextBean( data class VnextBean(var address: String = "",
var address: String = "", var port: Int = DEFAULT_PORT,
var port: Int = DEFAULT_PORT, var users: List<UsersBean>) {
var users: List<UsersBean>
) {
data class UsersBean( data class UsersBean(var id: String = "",
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( data class ServersBean(var address: String = "",
var packets: String? = null, var method: String = "chacha20-poly1305",
var length: String? = null, var ota: Boolean = false,
var interval: String? = null var password: String = "",
) var port: Int = DEFAULT_PORT,
var level: Int = DEFAULT_LEVEL,
val email: String? = null,
var flow: String? = null,
val ivCheck: Boolean? = null,
var users: List<SocksUsersBean>? = null) {
data class NoiseBean(
var type: String? = null,
var packet: String? = null,
var delay: String? = null
)
data class ServersBean( data class SocksUsersBean(var user: String = "",
var address: String = "", var pass: String = "",
var method: String? = null, var level: Int = DEFAULT_LEVEL)
var ota: Boolean = false,
var password: String? = null,
var port: Int = DEFAULT_PORT,
var level: Int = DEFAULT_LEVEL,
val email: String? = null,
var flow: String? = null,
val ivCheck: Boolean? = null,
var users: List<SocksUsersBean>? = null
) {
data class SocksUsersBean(
var user: String = "",
var pass: String = "",
var level: Int = DEFAULT_LEVEL
)
} }
data class Response(var type: String) data class Response(var type: String)
data class WireGuardBean(
var publicKey: String = "",
var endpoint: String = ""
)
} }
data class StreamSettingsBean( data class StreamSettingsBean(var network: String = DEFAULT_NETWORK,
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 httpSettings: HttpSettingsBean? = null,
var httpupgradeSettings: HttpupgradeSettingsBean? = null, var tlsSettings: TlsSettingsBean? = null,
var splithttpSettings: SplithttpSettingsBean? = null, var quicSettings: QuicSettingBean? = null,
var httpSettings: HttpSettingsBean? = null, var xtlsSettings: TlsSettingsBean? = null,
var tlsSettings: TlsSettingsBean? = null, var grpcSettings: GrpcSettingsBean? = null,
var quicSettings: QuicSettingBean? = null, val dsSettings: Any? = null,
var realitySettings: TlsSettingsBean? = null, val sockopt: Any? = null
var grpcSettings: GrpcSettingsBean? = null,
var hy2steriaSettings: Hy2steriaSettingsBean? = null,
val dsSettings: Any? = null,
var sockopt: SockoptBean? = null
) { ) {
data class TcpSettingsBean( data class TcpSettingsBean(var header: HeaderBean = HeaderBean(),
var header: HeaderBean = HeaderBean(), val acceptProxyProtocol: Boolean? = null) {
val acceptProxyProtocol: Boolean? = null data class HeaderBean(var type: String = "none",
) { var request: RequestBean? = null,
data class HeaderBean( var response: Any? = null) {
var type: String = "none", data class RequestBean(var path: List<String> = ArrayList(),
var request: RequestBean? = null, var headers: HeadersBean = HeadersBean(),
var response: Any? = null val version: String? = null,
) { val method: String? = null) {
data class RequestBean( data class HeadersBean(var Host: List<String> = ArrayList(),
var path: List<String> = ArrayList(), @SerializedName("User-Agent")
var headers: HeadersBean = HeadersBean(), val userAgent: List<String>? = null,
val version: String? = null, @SerializedName("Accept-Encoding")
val method: String? = null val acceptEncoding: List<String>? = null,
) { val Connection: List<String>? = null,
data class HeadersBean( val Pragma: String? = null)
var Host: List<String>? = ArrayList(),
@SerializedName("User-Agent")
val userAgent: List<String>? = null,
@SerializedName("Accept-Encoding")
val acceptEncoding: List<String>? = null,
val Connection: List<String>? = null,
val Pragma: String? = null
)
} }
} }
} }
data class KcpSettingsBean( data class KcpSettingsBean(var mtu: Int = 1350,
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, var congestion: Boolean = false,
var congestion: Boolean = false, 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( data class WsSettingsBean(var path: String = "",
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( data class HttpSettingsBean(var host: List<String> = ArrayList(),
var path: String = "", var path: String = "")
var host: String = "",
val acceptProxyProtocol: Boolean? = null
)
data class SplithttpSettingsBean( data class TlsSettingsBean(var allowInsecure: Boolean = false,
var path: String = "", var serverName: String = "",
var host: String = "", val alpn: List<String>? = null,
val maxUploadSize: Int? = null, val minVersion: String? = null,
val maxConcurrentUploads: Int? = null val maxVersion: String? = null,
) val preferServerCipherSuites: Boolean? = null,
val cipherSuites: String? = null,
val fingerprint: String? = null,
val certificates: List<Any>? = null,
val disableSystemRoot: Boolean? = null,
val enableSessionResumption: Boolean? = null)
data class HttpSettingsBean( data class QuicSettingBean(var security: String = "none",
var host: List<String> = ArrayList(), var key: String = "",
var path: String = "" var header: HeaderBean = HeaderBean()) {
)
data class SockoptBean(
var TcpNoDelay: Boolean? = null,
var tcpKeepAliveIdle: Int? = null,
var tcpFastOpen: Boolean? = null,
var tproxy: String? = null,
var mark: Int? = null,
var dialerProxy: String? = null
)
data class TlsSettingsBean(
var allowInsecure: Boolean = false,
var serverName: String = "",
val alpn: List<String>? = null,
val minVersion: String? = null,
val maxVersion: String? = null,
val preferServerCipherSuites: Boolean? = null,
val cipherSuites: String? = null,
val fingerprint: String? = null,
val certificates: List<Any>? = null,
val disableSystemRoot: Boolean? = null,
val enableSessionResumption: Boolean? = null,
// REALITY settings
val show: Boolean = false,
var publicKey: String? = null,
var shortId: String? = null,
var spiderX: String? = null
)
data class QuicSettingBean(
var security: String = "none",
var key: String = "",
var header: HeaderBean = HeaderBean()
) {
data class HeaderBean(var type: String = "none") data class HeaderBean(var type: String = "none")
} }
data class GrpcSettingsBean( data class GrpcSettingsBean(var serviceName: String = "",
var serviceName: String = "", var multiMode: Boolean? = null)
var authority: String? = null,
var multiMode: Boolean? = null,
var idle_timeout: Int? = null,
var health_check_timeout: Int? = null
)
data class Hy2steriaSettingsBean( fun populateTransportSettings(transport: String, headerType: String?, host: String?, path: String?, seed: String?,
var password: String? = null, quicSecurity: String?, key: String?, mode: String?, serviceName: String?): String {
var use_udp_extension: Boolean? = true,
var congestion: Hy2CongestionBean? = null
) {
data class Hy2CongestionBean(
var type: String? = "bbr",
var up_mbps: Int? = null,
var down_mbps: Int? = null,
)
}
fun populateTransportSettings(
transport: String, headerType: String?, host: String?, path: String?, seed: String?,
quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
authority: String?
): String {
var sni = "" var sni = ""
network = transport network = transport
when (network) { when (network) {
@@ -321,18 +205,17 @@ 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.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() } requestObj.headers.Host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
requestObj.path = (path.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() } requestObj.path = (path ?: "").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.orEmpty() sni = host ?: ""
} }
tcpSettings = tcpSetting tcpSettings = tcpSetting
} }
"kcp" -> { "kcp" -> {
val kcpsetting = KcpSettingsBean() val kcpsetting = KcpSettingsBean()
kcpsetting.header.type = headerType ?: "none" kcpsetting.header.type = headerType ?: "none"
@@ -343,151 +226,91 @@ data class V2rayConfig(
} }
kcpSettings = kcpsetting kcpSettings = kcpsetting
} }
"ws" -> { "ws" -> {
val wssetting = WsSettingsBean() val wssetting = WsSettingsBean()
wssetting.headers.Host = host.orEmpty() wssetting.headers.Host = host ?: ""
sni = wssetting.headers.Host sni = wssetting.headers.Host
wssetting.path = path ?: "/" wssetting.path = path ?: "/"
wsSettings = wssetting wsSettings = wssetting
} }
"httpupgrade" -> {
val httpupgradeSetting = HttpupgradeSettingsBean()
httpupgradeSetting.host = host.orEmpty()
sni = httpupgradeSetting.host
httpupgradeSetting.path = path ?: "/"
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.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() } h2Setting.host = (host ?: "").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.orEmpty() quicsetting.key = key ?: ""
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.orEmpty() grpcSetting.serviceName = serviceName ?: ""
grpcSetting.authority = authority.orEmpty() sni = host ?: ""
grpcSetting.idle_timeout = 60
grpcSetting.health_check_timeout = 20
sni = authority.orEmpty()
grpcSettings = grpcSetting grpcSettings = grpcSetting
} }
} }
return sni return sni
} }
fun populateTlsSettings( fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: 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,
serverName = sni, serverName = sni,
fingerprint = fingerprint, fingerprint = fingerprint
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
publicKey = publicKey,
shortId = shortId,
spiderX = spiderX
) )
if (security == TLS) { if (security == TLS) {
tlsSettings = tlsSetting tlsSettings = tlsSetting
realitySettings = null xtlsSettings = null
} else if (security == REALITY) { } else if (security == XTLS) {
tlsSettings = null tlsSettings = null
realitySettings = tlsSetting xtlsSettings = tlsSetting
} }
} }
} }
data class MuxBean( data class MuxBean(var enabled: Boolean, var concurrency: Int = 8)
var enabled: Boolean,
var concurrency: Int = 8,
var xudpConcurrency: Int = 8,
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.HTTP.name, true) || protocol.equals(EConfigType.TROJAN.name, true)) {
|| protocol.equals(EConfigType.TROJAN.name, true)
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
) {
return settings?.servers?.get(0)?.address return settings?.servers?.get(0)?.address
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.peers?.get(0)?.endpoint?.substringBeforeLast(":")
} }
return null return null
} }
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.HTTP.name, true) || protocol.equals(EConfigType.TROJAN.name, true)) {
|| protocol.equals(EConfigType.TROJAN.name, true)
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
) {
return settings?.servers?.get(0)?.port return settings?.servers?.get(0)?.port
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.peers?.get(0)?.endpoint?.substringAfterLast(":")?.toInt()
} }
return null return null
} }
fun getServerAddressAndPort(): String {
val address = getServerAddress().orEmpty()
val port = getServerPort()
return Utils.getIpv6Address(address) + ":" + port
}
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)) {
|| protocol.equals(EConfigType.HYSTERIA2.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)) {
|| protocol.equals(EConfigType.HTTP.name, true)
) {
return settings?.servers?.get(0)?.users?.get(0)?.pass return settings?.servers?.get(0)?.users?.get(0)?.pass
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.secretKey
} }
return null return null
} }
@@ -503,84 +326,46 @@ data class V2rayConfig(
fun getTransportSettingDetails(): List<String>? { fun getTransportSettingDetails(): List<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)
|| protocol.equals(EConfigType.TROJAN.name, true) || protocol.equals(EConfigType.TROJAN.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( listOf(tcpSetting.header.type,
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( listOf(kcpSetting.header.type,
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" -> {
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
listOf(
"",
httpupgradeSetting.host,
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( listOf(quicSetting.header.type,
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( listOf(if (grpcSetting.multiMode == true) "multi" else "gun",
if (grpcSetting.multiMode == true) "multi" else "gun", "",
grpcSetting.authority.orEmpty(), grpcSetting.serviceName)
grpcSetting.serviceName
)
} }
else -> null else -> null
} }
} }
@@ -588,74 +373,65 @@ data class V2rayConfig(
} }
} }
data class DnsBean( data class DnsBean(var servers: ArrayList<Any>? = null,
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( data class ServersBean(var address: String = "",
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,
var domainMatcher: String? = null,
var rules: ArrayList<RulesBean>,
val balancers: List<Any>? = null) {
data class RulesBean(var type: String = "",
var ip: ArrayList<String>? = null,
var domain: ArrayList<String>? = null,
var outboundTag: String = "",
var balancerTag: String? = null,
var port: String? = null,
val sourcePort: String? = null,
val network: String? = null,
val source: List<String>? = null,
val user: List<String>? = null,
var inboundTag: List<String>? = null,
val protocol: List<String>? = null,
val attrs: String? = null,
val domainMatcher: String? = null
) )
} }
data class RoutingBean( data class PolicyBean(var levels: Map<String, LevelBean>,
var domainStrategy: String, var system: Any? = null) {
var domainMatcher: String? = null,
var rules: ArrayList<RulesBean>,
val balancers: List<Any>? = null
) {
data class RulesBean(
var type: String = "field",
var ip: ArrayList<String>? = null,
var domain: ArrayList<String>? = null,
var outboundTag: String = "",
var balancerTag: String? = null,
var port: String? = null,
val sourcePort: String? = null,
val network: String? = null,
val source: List<String>? = null,
val user: List<String>? = null,
var inboundTag: List<String>? = null,
val protocol: List<String>? = null,
val attrs: String? = null,
val domainMatcher: String? = null
)
}
data class PolicyBean(
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,
var uplinkOnly: Int? = null, var uplinkOnly: Int? = null,
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( data class FakednsBean(var ipPool: String = "198.18.0.0/15",
var ipPool: String = "198.18.0.0/15", var poolSize: Int = 10000) // roughly 10 times smaller than total ip pool
var poolSize: Int = 10000
) // roughly 10 times smaller than total ip pool
fun getProxyOutbound(): OutboundBean? { fun getProxyOutbound(): OutboundBean? {
outbounds.forEach { outbound -> outbounds.forEach { outbound ->
EConfigType.entries.forEach { if (outbound.protocol.equals(EConfigType.VMESS.name, true) ||
if (outbound.protocol.equals(it.name, true)) { outbound.protocol.equals(EConfigType.VLESS.name, true) ||
return outbound outbound.protocol.equals(EConfigType.SHADOWSOCKS.name, true) ||
} outbound.protocol.equals(EConfigType.SOCKS.name, true) ||
outbound.protocol.equals(EConfigType.TROJAN.name, true)) {
return outbound
} }
} }
return null return null
@@ -663,13 +439,13 @@ data class V2rayConfig(
fun toPrettyPrinting(): String { fun toPrettyPrinting(): String {
return GsonBuilder() return GsonBuilder()
.setPrettyPrinting() .setPrettyPrinting()
.disableHtmlEscaping() .disableHtmlEscaping()
.registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start .registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
object : TypeToken<Double>() {}.type, object : TypeToken<Double>() {}.type,
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) } JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) }
) )
.create() .create()
.toJson(this) .toJson(this)
} }
} }
@@ -1,19 +1,15 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto
data class VmessQRCode( data class VmessQRCode(var v: String = "",
var v: String = "", var ps: String = "",
var ps: String = "", var add: String = "",
var add: String = "", var port: String = "",
var port: String = "", var id: String = "",
var id: String = "", var aid: String = "0",
var aid: String = "0", var scy: String = "",
var scy: String = "", var net: String = "",
var net: String = "", var type: String = "",
var type: String = "", var host: String = "",
var host: String = "", var path: String = "",
var path: String = "", var tls: String = "",
var tls: String = "", var sni: String = "")
var sni: String = "",
var alpn: String = "",
var fp: String = ""
)
@@ -1,96 +1,80 @@
package com.v2ray.ang.extension package com.v2ray.ang.extension
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build import android.os.Build
import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import com.v2ray.ang.AngApplication import com.v2ray.ang.AngApplication
import me.drakeet.support.toast.ToastCompat import me.drakeet.support.toast.ToastCompat
import org.json.JSONObject import org.json.JSONObject
import java.io.Serializable
import java.net.URI import java.net.URI
import java.net.URLConnection import java.net.URLConnection
val Context.v2RayApplication: AngApplication? /**
get() = applicationContext as? AngApplication * Some extensions
*/
fun Context.toast(message: Int) { val Context.v2RayApplication: AngApplication
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show() get() = applicationContext as AngApplication
}
fun Context.toast(message: CharSequence) { fun Context.toast(message: Int): Toast = ToastCompat
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show() .makeText(this, message, Toast.LENGTH_SHORT)
} .apply {
show()
}
fun JSONObject.putOpt(pair: Pair<String, Any?>) { fun Context.toast(message: CharSequence): Toast = ToastCompat
put(pair.first, pair.second) .makeText(this, message, Toast.LENGTH_SHORT)
} .apply {
show()
}
fun JSONObject.putOpt(pairs: Map<String, Any?>) { fun JSONObject.putOpt(pair: Pair<String, Any>) = putOpt(pair.first, pair.second)
pairs.forEach { put(it.key, it.value) } fun JSONObject.putOpt(pairs: Map<String, Any>) = pairs.forEach { putOpt(it.key to it.value) }
}
const val THRESHOLD = 1000L const val threshold = 1000
const val DIVISOR = 1024.0 const val divisor = 1024F
fun Long.toSpeedString(): String = this.toTrafficString() + "/s" fun Long.toSpeedString() = toTrafficString() + "/s"
fun Long.toTrafficString(): String { fun Long.toTrafficString(): String {
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB") if (this == 0L)
var size = this.toDouble() return "\t\t\t0\t B"
var unitIndex = 0
while (size >= THRESHOLD && unitIndex < units.size - 1) { if (this < threshold)
size /= DIVISOR return "${this.toFloat().toShortString()}\t B"
unitIndex++
} val kib = this / divisor
return String.format("%.1f %s", size, units[unitIndex]) if (kib < threshold)
return "${kib.toShortString()}\t KB"
val mib = kib / divisor
if (mib < threshold)
return "${mib.toShortString()}\t MB"
val gib = mib / divisor
if (gib < threshold)
return "${gib.toShortString()}\t GB"
val tib = gib / divisor
if (tib < threshold)
return "${tib.toShortString()}\t TB"
val pib = tib / divisor
if (pib < threshold)
return "${pib.toShortString()}\t PB"
return "∞"
}
private fun Float.toShortString(): String {
val s = "%.2f".format(this)
if (s.length <= 4)
return s
return s.substring(0, 4).removeSuffix(".")
} }
val URLConnection.responseLength: Long val URLConnection.responseLength: Long
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) contentLengthLong else contentLength.toLong()
contentLengthLong
} else {
contentLength.toLong()
}
val URI.idnHost: String val URI.idnHost: String
get() = host?.replace("[", "")?.replace("]", "").orEmpty() get() = (host!!).replace("[", "").replace("]", "")
fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "")
fun String.toLongEx(): Long = toLongOrNull() ?: 0
fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
callback()
if (onetime) context.unregisterReceiver(this)
}
}.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(this, IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}, Context.RECEIVER_EXPORTED)
} else {
registerReceiver(this, IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
})
}
}
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializable(key) as? T
}
inline fun <reified T : Serializable> Intent.serializable(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
}
@@ -1,32 +0,0 @@
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
******************************************************************************/
package com.v2ray.ang.plugin
import android.content.pm.ResolveInfo
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
init {
check(resolveInfo.providerInfo != null)
}
override val componentInfo get() = resolveInfo.providerInfo!!
}
@@ -1,43 +0,0 @@
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
******************************************************************************/
package com.v2ray.ang.plugin
import android.graphics.drawable.Drawable
abstract class Plugin {
abstract val id: String
abstract val label: CharSequence
abstract val version: Int
abstract val versionName: String
open val icon: Drawable? get() = null
open val defaultConfig: String? get() = null
open val packageName: String get() = ""
open val directBootAware: Boolean get() = true
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return id == (other as Plugin).id
}
override fun hashCode() = id.hashCode()
}
@@ -1,33 +0,0 @@
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
******************************************************************************/
package com.v2ray.ang.plugin
object PluginContract {
const val ACTION_NATIVE_PLUGIN = "io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN"
const val EXTRA_ENTRY = "io.nekohasekai.sagernet.plugin.EXTRA_ENTRY"
const val METADATA_KEY_ID = "io.nekohasekai.sagernet.plugin.id"
const val METADATA_KEY_EXECUTABLE_PATH = "io.nekohasekai.sagernet.plugin.executable_path"
const val METHOD_GET_EXECUTABLE = "sagernet:getExecutable"
const val COLUMN_PATH = "path"
const val COLUMN_MODE = "mode"
const val SCHEME = "plugin"
}
@@ -1,53 +0,0 @@
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
******************************************************************************/
package com.v2ray.ang.plugin
import android.content.Intent
import android.content.pm.PackageManager
import com.v2ray.ang.AngApplication
class PluginList : ArrayList<Plugin>() {
init {
addAll(
AngApplication.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA)
.filter { it.providerInfo.exported }.map { NativePlugin(it) })
}
val lookup = mutableMapOf<String, Plugin>().apply {
for (plugin in this@PluginList.toList()) {
fun check(old: Plugin?) {
if (old != null && old != plugin) {
this@PluginList.remove(old)
}
/* if (old != null && old !== plugin) {
val packages = this@PluginList.filter { it.id == plugin.id }
.joinToString { it.packageName }
val message = "Conflicting plugins found from: $packages"
Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()
throw IllegalStateException(message)
}*/
}
check(put(plugin.id, plugin))
}
}
}
@@ -1,219 +0,0 @@
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai <contact-AngApplication@sekai.icu> *
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
******************************************************************************/
package com.v2ray.ang.plugin
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ContentResolver
import android.content.Intent
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.content.pm.ProviderInfo
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.system.Os
import android.widget.Toast
import androidx.core.os.bundleOf
import com.v2ray.ang.AngApplication
import com.v2ray.ang.extension.listenForPackageChanges
import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID
import java.io.File
import java.io.FileNotFoundException
object PluginManager {
class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin)
private var receiver: BroadcastReceiver? = null
private var cachedPlugins: PluginList? = null
fun fetchPlugins() = synchronized(this) {
if (receiver == null) receiver = AngApplication.application.listenForPackageChanges {
synchronized(this) {
receiver = null
cachedPlugins = null
}
}
if (cachedPlugins == null) cachedPlugins = PluginList()
cachedPlugins!!
}
private fun buildUri(id: String, authority: String) = Uri.Builder()
.scheme(PluginContract.SCHEME)
.authority(authority)
.path("/$id")
.build()
data class InitResult(
val path: String,
)
@Throws(Throwable::class)
fun init(pluginId: String): InitResult? {
if (pluginId.isEmpty()) return null
var throwable: Throwable? = null
try {
val result = initNative(pluginId)
if (result != null) return result
} catch (t: Throwable) {
if (throwable == null) throwable = t //Logs.w(t)
}
throw throwable ?: PluginNotFoundException(pluginId)
}
private fun initNative(pluginId: String): InitResult? {
var flags = PackageManager.GET_META_DATA
if (Build.VERSION.SDK_INT >= 24) {
flags =
flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE
}
var providers = AngApplication.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "com.github.dyhkwong.AngApplication")), flags)
.filter { it.providerInfo.exported }
if (providers.isEmpty()) {
providers = AngApplication.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "io.nekohasekai.AngApplication")), flags)
.filter { it.providerInfo.exported }
}
if (providers.isEmpty()) {
providers = AngApplication.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "moe.matsuri.lite")), flags)
.filter { it.providerInfo.exported }
}
if (providers.isEmpty()) {
providers = AngApplication.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "fr.husi")), flags)
.filter { it.providerInfo.exported }
}
if (providers.isEmpty()) {
providers = AngApplication.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA
).filter {
it.providerInfo.exported &&
it.providerInfo.metaData.containsKey(METADATA_KEY_ID) &&
it.providerInfo.metaData.getString(METADATA_KEY_ID) == pluginId
}
if (providers.size > 1) {
providers = listOf(providers[0]) // What if there is more than one?
}
}
if (providers.isEmpty()) return null
if (providers.size > 1) {
val message =
"Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
Toast.makeText(AngApplication.application, message, Toast.LENGTH_LONG).show()
throw IllegalStateException(message)
}
val provider = providers.single().providerInfo
var failure: Throwable? = null
try {
initNativeFaster(provider)?.also { return InitResult(it) }
} catch (t: Throwable) {
// Logs.w("Initializing native plugin faster mode failed")
failure = t
}
val uri = Uri.Builder().apply {
scheme(ContentResolver.SCHEME_CONTENT)
authority(provider.authority)
}.build()
try {
return initNativeFast(AngApplication.application.contentResolver,
pluginId,
uri)?.let { InitResult(it) }
} catch (t: Throwable) {
// Logs.w("Initializing native plugin fast mode failed")
failure?.also { t.addSuppressed(it) }
failure = t
}
try {
return initNativeSlow(AngApplication.application.contentResolver,
pluginId,
uri)?.let { InitResult(it) }
} catch (t: Throwable) {
failure?.also { t.addSuppressed(it) }
throw t
}
}
private fun initNativeFaster(provider: ProviderInfo): String? {
return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH)
?.let { relativePath ->
File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply {
check(canExecute())
}.absolutePath
}
}
private fun initNativeFast(cr: ContentResolver, pluginId: String, uri: Uri): String? {
return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, bundleOf())
?.getString(PluginContract.EXTRA_ENTRY)?.also {
check(File(it).canExecute())
}
}
@SuppressLint("Recycle")
private fun initNativeSlow(cr: ContentResolver, pluginId: String, uri: Uri): String? {
var initialized = false
fun entryNotFound(): Nothing =
throw IndexOutOfBoundsException("Plugin entry binary not found")
val pluginDir = File(AngApplication.application.noBackupFilesDir, "plugin")
(cr.query(uri,
arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE),
null,
null,
null)
?: return null).use { cursor ->
if (!cursor.moveToFirst()) entryNotFound()
pluginDir.deleteRecursively()
if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory")
val pluginDirPath = pluginDir.absolutePath + '/'
do {
val path = cursor.getString(0)
val file = File(pluginDir, path)
check(file.absolutePath.startsWith(pluginDirPath))
cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->
file.outputStream().use { outStream -> inStream.copyTo(outStream) }
}
Os.chmod(file.absolutePath, when (cursor.getType(1)) {
Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1)
Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8)
else -> throw IllegalArgumentException("File mode should be of type int")
})
if (path == pluginId) initialized = true
} while (cursor.moveToNext())
}
if (!initialized) entryNotFound()
return File(pluginDir, pluginId).absolutePath
}
fun ComponentInfo.loadString(key: String) = when (val value = metaData.get(key)) {
is String -> value
is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
.getString(value)
null -> null
else -> error("meta-data $key has invalid type ${value.javaClass}")
}
}
@@ -1,45 +0,0 @@
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
******************************************************************************/
package com.v2ray.ang.plugin
import android.content.pm.ComponentInfo
import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable
import android.os.Build
import com.v2ray.ang.AngApplication
import com.v2ray.ang.plugin.PluginManager.loadString
abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
protected abstract val componentInfo: ComponentInfo
override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! }
override val version by lazy {
AngApplication.application.getPackageInfo(componentInfo.packageName).versionCode
}
override val versionName: String by lazy {
AngApplication.application.getPackageInfo(componentInfo.packageName).versionName!!
}
override val label: CharSequence get() = resolveInfo.loadLabel(AngApplication.application.packageManager)
override val icon: Drawable get() = resolveInfo.loadIcon(AngApplication.application.packageManager)
override val packageName: String get() = componentInfo.packageName
override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware
}
@@ -1,18 +0,0 @@
package com.v2ray.ang.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.MmkvManager
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (Intent.ACTION_BOOT_COMPLETED == intent?.action && MmkvManager.decodeStartOnBoot()) {
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
return
}
V2RayServiceManager.startV2Ray(context!!)
}
}
}
@@ -4,12 +4,16 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.text.TextUtils import android.text.TextUtils
import com.google.zxing.WriterException
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
class TaskerReceiver : BroadcastReceiver() { class TaskerReceiver : BroadcastReceiver() {
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) {
@@ -24,13 +28,13 @@ class TaskerReceiver : BroadcastReceiver() {
if (guid == AppConfig.TASKER_DEFAULT_GUID) { if (guid == AppConfig.TASKER_DEFAULT_GUID) {
Utils.startVServiceFromToggle(context) Utils.startVServiceFromToggle(context)
} else { } else {
MmkvManager.setSelectServer(guid) mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
V2RayServiceManager.startV2Ray(context) V2RayServiceManager.startV2Ray(context)
} }
} else { } else {
Utils.stopVService(context) Utils.stopVService(context)
} }
} catch (e: Exception) { } catch (e: WriterException) {
e.printStackTrace() e.printStackTrace()
} }
} }
@@ -8,8 +8,8 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.widget.RemoteViews import android.widget.RemoteViews
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.AppConfig
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
@@ -35,15 +35,20 @@ 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(
remoteViews.setInt(R.id.layout_background, "setBackgroundResource", R.drawable.ic_rounded_corner_active) R.id.layout_switch,
"setBackgroundResource",
R.drawable.ic_rounded_corner_theme
)
} else { } else {
remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_play_24dp) remoteViews.setInt(
remoteViews.setInt(R.id.layout_background, "setBackgroundResource", R.drawable.ic_rounded_corner_inactive) R.id.layout_switch,
"setBackgroundResource",
R.drawable.ic_rounded_corner_grey
)
} }
for (appWidgetId in appWidgetIds) { for (appWidgetId in appWidgetIds) {
@@ -66,17 +71,12 @@ 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( updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)), true)
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( updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)), false)
false
)
} }
} }
} }
@@ -1,45 +0,0 @@
package com.v2ray.ang.service
import android.content.Context
import android.util.Log
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ProcessService {
private val TAG = ANG_PACKAGE
private lateinit var process: Process
fun runProcess(context: Context, cmd: MutableList<String>) {
Log.d(TAG, cmd.toString())
try {
val proBuilder = ProcessBuilder(cmd)
proBuilder.redirectErrorStream(true)
process = proBuilder
.directory(context.filesDir)
.start()
CoroutineScope(Dispatchers.IO).launch {
Thread.sleep(50L)
Log.d(TAG, "runProcess check")
process.waitFor()
Log.d(TAG, "runProcess exited")
}
Log.d(TAG, process.toString())
} catch (e: Exception) {
Log.d(TAG, e.toString())
}
}
fun stopProcess() {
try {
Log.d(TAG, "runProcess destroy")
process?.destroy()
} catch (e: Exception) {
Log.d(TAG, e.toString())
}
}
}
@@ -36,12 +36,7 @@ class QSTileService : TileService() {
super.onStartListening() super.onStartListening()
setState(Tile.STATE_INACTIVE) setState(Tile.STATE_INACTIVE)
mMsgReceive = ReceiveMessageHandler(this) mMsgReceive = ReceiveMessageHandler(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY), Context.RECEIVER_EXPORTED)
} else {
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
}
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "") MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
} }
@@ -58,7 +53,6 @@ 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)
} }
@@ -68,26 +62,22 @@ 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() {
var mReference: SoftReference<QSTileService> = SoftReference(context) internal 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)
} }
@@ -1,69 +0,0 @@
package com.v2ray.ang.service
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME
import com.v2ray.ang.R
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
object SubscriptionUpdater {
class UpdateTask(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
private val notificationManager = NotificationManagerCompat.from(applicationContext)
private val notification =
NotificationCompat.Builder(applicationContext, SUBSCRIPTION_UPDATE_CHANNEL)
.setWhen(0)
.setTicker("Update")
.setContentTitle(context.getString(R.string.title_pref_auto_update_subscription))
.setSmallIcon(R.drawable.ic_stat_name)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
@SuppressLint("MissingPermission")
override suspend fun doWork(): Result {
Log.d(AppConfig.ANG_PACKAGE, "subscription automatic update starting")
val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate }
for (sub in subs) {
val subItem = sub.second
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notification.setChannelId(SUBSCRIPTION_UPDATE_CHANNEL)
val channel =
NotificationChannel(
SUBSCRIPTION_UPDATE_CHANNEL,
SUBSCRIPTION_UPDATE_CHANNEL_NAME,
NotificationManager.IMPORTANCE_MIN
)
notificationManager.createNotificationChannel(channel)
}
notificationManager.notify(3, notification.build())
Log.d(
AppConfig.ANG_PACKAGE,
"subscription automatic update: ---${subItem.remarks}"
)
updateConfigViaSub(Pair(sub.first, subItem))
notification.setContentText("Updating ${subItem.remarks}")
}
notificationManager.cancel(3)
return Result.success()
}
}
}
@@ -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()) MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
} }
super.attachBaseContext(context) super.attachBaseContext(context)
} }
@@ -1,9 +1,6 @@
package com.v2ray.ang.service package com.v2ray.ang.service
import android.app.Notification import android.app.*
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -13,10 +10,10 @@ import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
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.ANG_PACKAGE
import com.v2ray.ang.AppConfig.TAG_DIRECT import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.VPN
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.dto.ServerConfig import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.extension.toSpeedString import com.v2ray.ang.extension.toSpeedString
@@ -24,19 +21,17 @@ import com.v2ray.ang.extension.toast
import com.v2ray.ang.ui.MainActivity import com.v2ray.ang.ui.MainActivity
import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.PluginUtil
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
@@ -48,32 +43,29 @@ object V2RayServiceManager {
val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
private val mMsgReceive = ReceiveMessageHandler() private val mMsgReceive = ReceiveMessageHandler()
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
var serviceControl: SoftReference<ServiceControl>? = null var serviceControl: SoftReference<ServiceControl>? = null
set(value) { set(value) {
field = value field = value
Seq.setContext(value?.get()?.getService()?.applicationContext) Seq.setContext(value?.get()?.getService()?.applicationContext)
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey()) Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()))
} }
var currentConfig: ServerConfig? = null var currentConfig: ServerConfig? = null
private var lastQueryTime = 0L private var lastQueryTime = 0L
private var mBuilder: NotificationCompat.Builder? = null private var mBuilder: NotificationCompat.Builder? = null
private var mDisposable: Disposable? = null private var mSubscription: Subscription? = 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 = MmkvManager.getSelectServer() ?: 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)
@@ -108,11 +100,13 @@ object V2RayServiceManager {
} }
override fun onEmitStatus(l: Long, s: String?): Long { override fun onEmitStatus(l: Long, s: String?): Long {
//Logger.d(s)
return 0 return 0
} }
override fun setup(s: String): Long { override fun setup(s: String): Long {
val serviceControl = serviceControl?.get() ?: return -1 val serviceControl = serviceControl?.get() ?: return -1
//Logger.d(s)
return try { return try {
serviceControl.startService() serviceControl.startService()
lastQueryTime = System.currentTimeMillis() lastQueryTime = System.currentTimeMillis()
@@ -127,47 +121,40 @@ object V2RayServiceManager {
fun startV2rayPoint() { fun startV2rayPoint() {
val service = serviceControl?.get()?.getService() ?: return val service = serviceControl?.get()?.getService() ?: return
val guid = MmkvManager.getSelectServer() ?: 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)
} if (!result.status)
val result = V2rayConfigUtil.getV2rayConfig(service, guid) return
if (!result.status)
return
try { try {
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE) val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
mFilter.addAction(Intent.ACTION_SCREEN_ON) mFilter.addAction(Intent.ACTION_SCREEN_ON)
mFilter.addAction(Intent.ACTION_SCREEN_OFF) mFilter.addAction(Intent.ACTION_SCREEN_OFF)
mFilter.addAction(Intent.ACTION_USER_PRESENT) mFilter.addAction(Intent.ACTION_USER_PRESENT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
service.registerReceiver(mMsgReceive, mFilter, Context.RECEIVER_EXPORTED)
} else {
service.registerReceiver(mMsgReceive, mFilter) service.registerReceiver(mMsgReceive, mFilter)
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
} }
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
}
v2rayPoint.configureFileContent = result.content v2rayPoint.configureFileContent = result.content
v2rayPoint.domainName = result.domainPort v2rayPoint.domainName = config.getV2rayPointDomainAndPort()
currentConfig = config currentConfig = config
try { try {
v2rayPoint.runLoop(settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) ?: false) v2rayPoint.runLoop(settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) ?: false)
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.d(ANG_PACKAGE, e.toString())
} }
if (v2rayPoint.isRunning) { if (v2rayPoint.isRunning) {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "") MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
showNotification() showNotification()
} else {
PluginUtil.runPlugin(service, config, result.domainPort) MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
} else { cancelNotification()
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "") }
cancelNotification()
} }
} }
@@ -175,7 +162,7 @@ object V2RayServiceManager {
val service = serviceControl?.get()?.getService() ?: return val service = serviceControl?.get()?.getService() ?: return
if (v2rayPoint.isRunning) { if (v2rayPoint.isRunning) {
CoroutineScope(Dispatchers.IO).launch { GlobalScope.launch(Dispatchers.Default) {
try { try {
v2rayPoint.stopLoop() v2rayPoint.stopLoop()
} catch (e: Exception) { } catch (e: Exception) {
@@ -192,7 +179,6 @@ object V2RayServiceManager {
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.d(ANG_PACKAGE, e.toString())
} }
PluginUtil.stopPlugin()
} }
private class ReceiveMessageHandler : BroadcastReceiver() { private class ReceiveMessageHandler : BroadcastReceiver() {
@@ -200,29 +186,25 @@ object V2RayServiceManager {
val serviceControl = serviceControl?.get() ?: return val serviceControl = serviceControl?.get() ?: return
when (intent?.getIntExtra("key", 0)) { when (intent?.getIntExtra("key", 0)) {
AppConfig.MSG_REGISTER_CLIENT -> { AppConfig.MSG_REGISTER_CLIENT -> {
//Logger.e("ReceiveMessageHandler", intent?.getIntExtra("key", 0).toString())
if (v2rayPoint.isRunning) { if (v2rayPoint.isRunning) {
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "") MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
} else { } else {
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()
} }
@@ -233,7 +215,6 @@ 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()
@@ -243,25 +224,17 @@ object V2RayServiceManager {
} }
private fun measureV2rayDelay() { private fun measureV2rayDelay() {
CoroutineScope(Dispatchers.IO).launch { GlobalScope.launch(Dispatchers.IO) {
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(Utils.getDelayTestUrl()) time = v2rayPoint.measureDelay()
} 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)
@@ -276,52 +249,46 @@ 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( val contentPendingIntent = PendingIntent.getActivity(service,
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( val stopV2RayPendingIntent = PendingIntent.getBroadcast(service,
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) {
createNotificationChannel() createNotificationChannel()
} else { } else {
// If earlier version channel ID is not used // If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
"" ""
} }
mBuilder = NotificationCompat.Builder(service, channelId) mBuilder = NotificationCompat.Builder(service, channelId)
.setSmallIcon(R.drawable.ic_stat_name) .setSmallIcon(R.drawable.ic_stat_name)
.setContentTitle(currentConfig?.remarks) .setContentTitle(currentConfig?.remarks)
.setPriority(NotificationCompat.PRIORITY_MIN) .setPriority(NotificationCompat.PRIORITY_MIN)
.setOngoing(true) .setOngoing(true)
.setShowWhen(false) .setShowWhen(false)
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
.setContentIntent(contentPendingIntent) .setContentIntent(contentPendingIntent)
.addAction( .addAction(R.drawable.ic_close_grey_800_24dp,
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) //取消震动,铃声其他都不好使
@@ -331,12 +298,10 @@ object V2RayServiceManager {
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(): String { private fun createNotificationChannel(): String {
val channelId = AppConfig.RAY_NG_CHANNEL_ID val channelId = "RAY_NG_M_CH_ID"
val channelName = AppConfig.RAY_NG_CHANNEL_NAME val channelName = "V2rayNG Background Service"
val chan = NotificationChannel( val chan = NotificationChannel(channelId,
channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
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
@@ -348,8 +313,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
mDisposable?.dispose() mSubscription?.unsubscribe()
mDisposable = null mSubscription = null
} }
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) { private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
@@ -376,44 +341,41 @@ object V2RayServiceManager {
} }
private fun startSpeedNotification() { private fun startSpeedNotification() {
if (mDisposable == null && if (mSubscription == 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)
mDisposable = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS) mSubscription = 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, AppConfig.UPLINK) val up = v2rayPoint.queryStats(it, "uplink")
val down = v2rayPoint.queryStats(it, AppConfig.DOWNLINK) val down = v2rayPoint.queryStats(it, "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,
directDownlink / sinceLastQueryInSeconds)
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
} }
appendSpeedString( lastZeroSpeed = zeroSpeed
text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds, lastQueryTime = queryTime
directDownlink / sinceLastQueryInSeconds
)
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
} }
lastZeroSpeed = zeroSpeed
lastQueryTime = queryTime
}
} }
} }
@@ -428,9 +390,9 @@ object V2RayServiceManager {
} }
private fun stopSpeedNotification() { private fun stopSpeedNotification() {
if (mDisposable != null) { if (mSubscription != null) {
mDisposable?.dispose() //stop queryStats mSubscription?.unsubscribe() //stop queryStats
mDisposable = null mSubscription = null
updateNotification(currentConfig?.remarks, 0, 0) updateNotification(currentConfig?.remarks, 0, 0)
} }
} }
@@ -6,20 +6,11 @@ import android.os.IBinder
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.serializable
import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.PluginUtil
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 com.v2ray.ang.util.V2rayConfigUtil
import go.Seq import go.Seq
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
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
@@ -29,19 +20,18 @@ class V2RayTestService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Seq.setContext(this) Seq.setContext(this)
Libv2ray.initV2Env(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey()) Libv2ray.initV2Env(Utils.userAssetPath(this))
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.getIntExtra("key", 0)) { when (intent?.getIntExtra("key", 0)) {
MSG_MEASURE_CONFIG -> { MSG_MEASURE_CONFIG -> {
val guid = intent.serializable<String>("content") ?: "" val contentPair = intent.getSerializableExtra("content") as Pair<String, String>
realTestScope.launch { realTestScope.launch {
val result = startRealPing(guid) val result = SpeedtestUtil.realPing(contentPair.second)
MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, 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()
} }
@@ -52,20 +42,4 @@ class V2RayTestService : Service() {
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
return null return null
} }
private fun startRealPing(guid: String): Long {
val retFailure = -1L
val server = MmkvManager.decodeServerConfig(guid) ?: return retFailure
if (server.getProxyOutbound()?.protocol?.equals(EConfigType.HYSTERIA2.name, true) == true) {
val delay = PluginUtil.realPingHy2(this, server)
return delay
} else {
val config = V2rayConfigUtil.getV2rayConfig(this, guid)
if (!config.status) {
return retFailure
}
return SpeedtestUtil.realPing(config.content)
}
}
} }
@@ -4,28 +4,21 @@ 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.ConnectivityManager import android.net.*
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
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.util.MmkvManager.settingsStorage import com.v2ray.ang.dto.ERoutingMode
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MyContextWrapper import com.v2ray.ang.util.MyContextWrapper
import com.v2ray.ang.util.SettingsManager
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
@@ -40,9 +33,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
private const val TUN2SOCKS = "libtun2socks.so" private const val TUN2SOCKS = "libtun2socks.so"
} }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
private lateinit var mInterface: ParcelFileDescriptor private lateinit var mInterface: ParcelFileDescriptor
private var isRunning = false
//val fd: Int get() = mInterface.fd //val fd: Int get() = mInterface.fd
private lateinit var process: Process private lateinit var process: Process
@@ -59,9 +52,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
@delegate:RequiresApi(Build.VERSION_CODES.P) @delegate:RequiresApi(Build.VERSION_CODES.P)
private val defaultNetworkRequest by lazy { private val defaultNetworkRequest by lazy {
NetworkRequest.Builder() NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
.build() .build()
} }
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
@@ -117,11 +110,12 @@ class V2RayVpnService : VpnService(), ServiceControl {
val builder = Builder() val builder = Builder()
//val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false) //val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value
builder.setMtu(VPN_MTU) builder.setMtu(VPN_MTU)
builder.addAddress(PRIVATE_VLAN4_CLIENT, 30) builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER) //builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
val bypassLan = SettingsManager.routingRulesetsBypassLan() if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
if (bypassLan) {
resources.getStringArray(R.array.bypass_private_ip_address).forEach { resources.getStringArray(R.array.bypass_private_ip_address).forEach {
val addr = it.split('/') val addr = it.split('/')
builder.addRoute(addr[0], addr[1].toInt()) builder.addRoute(addr[0], addr[1].toInt())
@@ -132,32 +126,29 @@ class V2RayVpnService : VpnService(), ServiceControl {
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) { if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126) builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
if (bypassLan) { if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
} else { } else {
builder.addRoute("::", 0) builder.addRoute("::", 0)
} }
} }
// if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
// builder.addDnsServer(PRIVATE_VLAN4_ROUTER) builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
// } else { } else {
Utils.getVpnDnsServers() Utils.getVpnDnsServers()
.forEach { .forEach {
if (Utils.isPureIpAddress(it)) { if (Utils.isPureIpAddress(it)) {
builder.addDnsServer(it) builder.addDnsServer(it)
} }
} }
// } }
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty()) builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
val selfPackageName = BuildConfig.APPLICATION_ID
if (settingsStorage?.decodeBool(AppConfig.PREF_PER_APP_PROXY) == true) { if (settingsStorage?.decodeBool(AppConfig.PREF_PER_APP_PROXY) == true) {
val apps = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET) val apps = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
val bypassApps = settingsStorage?.decodeBool(AppConfig.PREF_BYPASS_APPS) ?: false val bypassApps = settingsStorage?.decodeBool(AppConfig.PREF_BYPASS_APPS) ?: false
//process self package
if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName)
apps?.forEach { apps?.forEach {
try { try {
if (bypassApps) if (bypassApps)
@@ -165,10 +156,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
else else
builder.addAllowedApplication(it) builder.addAllowedApplication(it)
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
//Logger.d(e)
} }
} }
} else {
builder.addDisallowedApplication(selfPackageName)
} }
// Close the old interface since the parameters have been changed. // Close the old interface since the parameters have been changed.
@@ -193,7 +183,6 @@ class V2RayVpnService : VpnService(), ServiceControl {
// Create a new interface using the builder and save the parameters. // Create a new interface using the builder and save the parameters.
try { try {
mInterface = builder.establish()!! mInterface = builder.establish()!!
isRunning = true
runTun2socks() runTun2socks()
} catch (e: Exception) { } catch (e: Exception) {
// non-nullable lateinit var // non-nullable lateinit var
@@ -203,17 +192,15 @@ class V2RayVpnService : VpnService(), ServiceControl {
} }
private fun runTun2socks() { private fun runTun2socks() {
val socksPort = SettingsManager.getSocksPort() val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
val cmd = arrayListOf( val cmd = arrayListOf(File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
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", "$LOOPBACK:${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")
@@ -222,7 +209,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt()) val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
cmd.add("--dnsgw") cmd.add("--dnsgw")
cmd.add("$LOOPBACK:${localDnsPort}") cmd.add("127.0.0.1:${localDnsPort}")
} }
Log.d(packageName, cmd.toString()) Log.d(packageName, cmd.toString())
@@ -230,17 +217,8 @@ class V2RayVpnService : VpnService(), ServiceControl {
val proBuilder = ProcessBuilder(cmd) val proBuilder = ProcessBuilder(cmd)
proBuilder.redirectErrorStream(true) proBuilder.redirectErrorStream(true)
process = proBuilder process = proBuilder
.directory(applicationContext.filesDir) .directory(applicationContext.filesDir)
.start() .start()
Thread(Runnable {
Log.d(packageName, "$TUN2SOCKS check")
process.waitFor()
Log.d(packageName, "$TUN2SOCKS exited")
if (isRunning) {
Log.d(packageName, "$TUN2SOCKS restart")
runTun2socks()
}
}).start()
Log.d(packageName, process.toString()) Log.d(packageName, process.toString())
sendFd() sendFd()
@@ -254,7 +232,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)
CoroutineScope(Dispatchers.IO).launch { GlobalScope.launch(Dispatchers.IO) {
var tries = 0 var tries = 0
while (true) try { while (true) try {
Thread.sleep(50L shl tries) Thread.sleep(50L shl tries)
@@ -284,7 +262,6 @@ 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
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)
@@ -337,7 +314,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()) MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
} }
super.attachBaseContext(context) super.attachBaseContext(context)
} }
@@ -1,175 +0,0 @@
package com.v2ray.ang.ui
import android.Manifest
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import com.tbruyelle.rxpermissions3.RxPermissions
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityAboutBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.ZipUtil
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
class AboutActivity : BaseActivity() {
private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) }
private val extDir by lazy { File(Utils.backupPath(this)) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
title = getString(R.string.title_about)
binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir)
binding.layoutBackup.setOnClickListener {
val ret = backupConfiguration(extDir.absolutePath)
if (ret.first) {
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 {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
RxPermissions(this)
.request(permission)
.subscribe {
if (it) {
try {
showFileChooser()
} catch (e: Exception) {
e.printStackTrace()
}
} else
toast(R.string.toast_permission_denied)
}
}
binding.layoutSoureCcode.setOnClickListener {
Utils.openUri(this, AppConfig.v2rayNGUrl)
}
binding.layoutFeedback.setOnClickListener {
Utils.openUri(this, AppConfig.v2rayNGIssues)
}
binding.layoutTgChannel.setOnClickListener {
Utils.openUri(this, AppConfig.TgChannelUrl)
}
binding.layoutPrivacyPolicy.setOnClickListener {
Utils.openUri(this, AppConfig.v2rayNGPrivacyPolicy)
}
"v${BuildConfig.VERSION_NAME} (${SpeedtestUtil.getLibVersion()})".also {
binding.tvVersion.text = it
}
}
fun backupConfiguration(outputZipFilePos: String): Pair<Boolean, String> {
val dateFormated = SimpleDateFormat(
"yyyy-MM-dd-HH-mm-ss",
Locale.getDefault()
).format(System.currentTimeMillis())
val folderName = "${getString(R.string.app_name)}_${dateFormated}"
val backupDir = this.cacheDir.absolutePath + "/$folderName"
val outputZipFilePath = "$outputZipFilePos/$folderName.zip"
val count = MMKV.backupAllToDirectory(backupDir)
if (count <= 0) {
return Pair(false, "")
}
if (ZipUtil.zipFromFolder(backupDir, outputZipFilePath)) {
return Pair(true, outputZipFilePath)
} else {
return Pair(false, "")
}
}
fun restoreConfiguration(zipFile: File): Boolean {
val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}"
if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
return false
}
val count = MMKV.restoreAllFromDirectory(backupDir)
return count > 0
}
private fun showFileChooser() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
try {
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
} catch (ex: android.content.ActivityNotFoundException) {
toast(R.string.toast_require_file_manager)
}
}
private val chooseFile =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
try {
try {
val targetFile =
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
contentResolver.openInputStream(uri).use { input ->
targetFile.outputStream().use { fileOut ->
input?.copyTo(fileOut)
}
}
if (restoreConfiguration(targetFile)) {
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
} catch (e: Exception) {
e.printStackTrace()
}
} catch (e: Exception) {
e.printStackTrace()
toast(e.message.toString())
}
}
}
}
@@ -3,40 +3,51 @@ package com.v2ray.ang.ui
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.view.MenuItem import android.view.MenuItem
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import com.v2ray.ang.util.MyContextWrapper import com.v2ray.ang.util.MyContextWrapper
import com.v2ray.ang.R
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
abstract class BaseActivity : AppCompatActivity() { abstract class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (!Utils.getDarkModeStatus(this)) {
WindowCompat.getInsetsController(window, window.decorView).apply {
isAppearanceLightStatusBars = true
}
}
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
// Handles the home button press by delegating to the onBackPressedDispatcher. onBackPressed()
// This ensures consistent back navigation behavior.
onBackPressedDispatcher.onBackPressed()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkDarkMode()
}
private fun checkDarkMode() {
if (Utils.getDarkModeStatus(this)) {
if (this.javaClass.simpleName == "MainActivity") {
setTheme(R.style.AppThemeDark_NoActionBar)
} else {
setTheme(R.style.AppThemeDark)
}
} else {
if (this.javaClass.simpleName == "MainActivity") {
setTheme(R.style.AppThemeLight_NoActionBar)
} else {
setTheme(R.style.AppThemeLight)
}
}
}
@RequiresApi(Build.VERSION_CODES.N) @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()) MyContextWrapper.wrap(newBase, Utils.getLocale(newBase))
} }
super.attachBaseContext(context) super.attachBaseContext(context)
} }
}
}
@@ -5,7 +5,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
class FragmentAdapter(fragmentActivity: FragmentActivity, private val mFragments: List<Fragment>) : class FragmentAdapter(fragmentActivity: FragmentActivity, private val mFragments: List<Fragment>) :
FragmentStateAdapter(fragmentActivity) { FragmentStateAdapter(fragmentActivity) {
override fun createFragment(position: Int): Fragment { override fun createFragment(position: Int): Fragment {
return mFragments[position] return mFragments[position]
@@ -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,21 +14,24 @@ 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 val binding by lazy { private lateinit var binding: ActivityLogcatBinding
ActivityLogcatBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) binding = ActivityLogcatBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
title = getString(R.string.title_logcat) title = getString(R.string.title_logcat)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
logcat(false) logcat(false)
} }
@@ -42,10 +45,8 @@ 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")
@@ -54,9 +55,7 @@ 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 = withContext(Dispatchers.IO) { val process = Runtime.getRuntime().exec(lst.toTypedArray())
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)
@@ -84,12 +83,10 @@ 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)
} }
} }
@@ -1,95 +1,74 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.Manifest import android.Manifest
import android.content.ActivityNotFoundException import android.content.*
import android.content.Intent
import android.content.res.ColorStateList
import android.net.Uri import android.net.Uri
import android.net.VpnService import android.net.VpnService
import android.os.Build import androidx.recyclerview.widget.LinearLayoutManager
import android.view.Menu
import android.view.MenuItem
import com.tbruyelle.rxpermissions.RxPermissions
import com.v2ray.ang.R
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu import com.v2ray.ang.AppConfig
import android.view.MenuItem import android.content.res.ColorStateList
import android.widget.Toast import com.google.android.material.navigation.NavigationView
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.isVisible import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import android.util.Log
import com.google.android.material.navigation.NavigationView import android.widget.Toast
import com.google.android.material.tabs.TabLayout import androidx.activity.result.contract.ActivityResultContracts
import com.tbruyelle.rxpermissions3.RxPermissions import androidx.activity.viewModels
import com.v2ray.ang.AppConfig import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.AppConfig.VPN import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.R import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.databinding.ActivityMainBinding import com.v2ray.ang.databinding.ActivityMainBinding
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.AngConfigManager import com.v2ray.ang.util.*
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
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 kotlinx.coroutines.*
import io.reactivex.rxjava3.core.Observable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.drakeet.support.toast.ToastCompat import me.drakeet.support.toast.ToastCompat
import java.util.concurrent.TimeUnit import java.io.File
import java.io.FileOutputStream
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener { class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
private val binding by lazy { private lateinit var binding: ActivityMainBinding
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)
setContentView(binding.root) binding = ActivityMainBinding.inflate(layoutInflater)
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 (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()
@@ -113,38 +92,21 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) val callback = SimpleItemTouchHelperCallback(adapter)
mItemTouchHelper = ItemTouchHelper(callback)
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView) mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
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)
binding.version.text = "v${BuildConfig.VERSION_NAME} (${SpeedtestUtil.getLibVersion()})"
initGroupTab()
setupViewModel() setupViewModel()
copyAssets()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { migrateLegacy()
RxPermissions(this)
.request(Manifest.permission.POST_NOTIFICATIONS)
.subscribe {
if (!it)
toast(R.string.toast_permission_denied)
}
}
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
binding.drawerLayout.closeDrawer(GravityCompat.START)
} else {
//super.onBackPressed()
onBackPressedDispatcher.onBackPressed()
}
}
})
} }
private fun setupViewModel() { private fun setupViewModel() {
@@ -159,50 +121,66 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
mainViewModel.isRunning.observe(this) { isRunning -> mainViewModel.isRunning.observe(this) { isRunning ->
adapter.isRunning = isRunning adapter.isRunning = isRunning
if (isRunning) { if (isRunning) {
binding.fab.setImageResource(R.drawable.ic_stop_24dp) binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorSelected))
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_active))
setTestState(getString(R.string.connection_connected)) setTestState(getString(R.string.connection_connected))
binding.layoutTest.isFocusable = true binding.layoutTest.isFocusable = true
} else { } else {
binding.fab.setImageResource(R.drawable.ic_play_24dp) binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorUnselected))
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
setTestState(getString(R.string.connection_not_connected)) setTestState(getString(R.string.connection_not_connected))
binding.layoutTest.isFocusable = false binding.layoutTest.isFocusable = false
} }
hideCircle()
} }
mainViewModel.startListenBroadcast() mainViewModel.startListenBroadcast()
mainViewModel.copyAssets(assets)
} }
private fun initGroupTab() { private fun copyAssets() {
binding.tabGroup.removeOnTabSelectedListener(tabGroupListener) val extFolder = Utils.userAssetPath(this)
binding.tabGroup.removeAllTabs() lifecycleScope.launch(Dispatchers.IO) {
binding.tabGroup.isVisible = false try {
val geo = arrayOf("geosite.dat", "geoip.dat")
val (listId, listRemarks) = mainViewModel.getSubscriptions(this) assets.list("")
if (listId == null || listRemarks == null) { ?.filter { geo.contains(it) }
return ?.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)
}
} }
}
for (it in listRemarks.indices) { private fun migrateLegacy() {
val tab = binding.tabGroup.newTab() lifecycleScope.launch(Dispatchers.IO) {
tab.text = listRemarks[it] val result = AngConfigManager.migrateLegacyConfig(this@MainActivity)
tab.tag = listId[it] if (result != null) {
binding.tabGroup.addTab(tab) launch(Dispatchers.Main) {
if (result) {
toast(getString(R.string.migration_success))
mainViewModel.reloadServerList()
} else {
toast(getString(R.string.migration_fail))
}
}
}
} }
val selectIndex =
listId.indexOf(mainViewModel.subscriptionId).takeIf { it >= 0 } ?: (listId.count() - 1)
binding.tabGroup.selectTab(binding.tabGroup.getTabAt(selectIndex))
binding.tabGroup.addOnTabSelectedListener(tabGroupListener)
binding.tabGroup.isVisible = true
} }
fun startV2Ray() { fun startV2Ray() {
if (MmkvManager.getSelectServer().isNullOrEmpty()) { if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
toast(R.string.title_file_chooser)
return return
} }
showCircle()
// toast(R.string.toast_services_start)
V2RayServiceManager.startV2Ray(this) V2RayServiceManager.startV2Ray(this)
hideCircle()
} }
fun restartV2Ray() { fun restartV2Ray() {
@@ -210,10 +188,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
Utils.stopVService(this) Utils.stopVService(this)
} }
Observable.timer(500, TimeUnit.MILLISECONDS) Observable.timer(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe {
startV2Ray() startV2Ray()
} }
} }
public override fun onResume() { public override fun onResume() {
@@ -227,25 +205,7 @@ 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) {
@@ -253,90 +213,63 @@ 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_http -> {
importManually(EConfigType.HTTP.value)
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 -> {
importManually(EConfigType.WIREGUARD.value)
true
}
R.id.import_manually_hysteria2 -> {
importManually(EConfigType.HYSTERIA2.value)
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 -> {
binding.pbWaiting.show() if (AngConfigManager.shareNonCustomConfigsToClipboard(this, mainViewModel.serverList) == 0) {
lifecycleScope.launch(Dispatchers.IO) { toast(R.string.toast_success)
val ret = mainViewModel.exportAllServer() } else {
launch(Dispatchers.Main) { toast(R.string.toast_failure)
if (ret == 0)
toast(R.string.toast_success)
else
toast(R.string.toast_failure)
binding.pbWaiting.hide()
}
} }
true true
} }
@@ -357,78 +290,37 @@ 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) { _, _ ->
binding.pbWaiting.show() MmkvManager.removeAllServer()
lifecycleScope.launch(Dispatchers.IO) { mainViewModel.reloadServerList()
mainViewModel.removeAllServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
binding.pbWaiting.hide()
}
} }
} .show()
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting
}
.show()
true
}
R.id.del_duplicate_config -> {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
val ret = mainViewModel.removeDuplicateServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
toast(getString(R.string.title_del_duplicate_config_count, ret))
binding.pbWaiting.hide()
}
}
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting
}
.show()
true true
} }
R.id.del_invalid_config -> { R.id.del_invalid_config -> {
AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm) AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
binding.pbWaiting.show() MmkvManager.removeInvalidServer()
lifecycleScope.launch(Dispatchers.IO) { mainViewModel.reloadServerList()
mainViewModel.removeInvalidServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
binding.pbWaiting.hide()
}
}
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting
} }
.show() .show()
true true
} }
R.id.sort_by_test_results -> { R.id.sort_by_test_results -> {
binding.pbWaiting.show() MmkvManager.sortByTestResults()
lifecycleScope.launch(Dispatchers.IO) { mainViewModel.reloadServerList()
mainViewModel.sortByTestResults() true
launch(Dispatchers.Main) { }
mainViewModel.reloadServerList() R.id.filter_config -> {
binding.pbWaiting.hide() 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)
@@ -440,23 +332,23 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/** /**
* import config from qrcode * import config from qrcode
*/ */
private fun importQRcode(forConfig: Boolean): Boolean { 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)
// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode) // .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
// } catch (e: Exception) { // } catch (e: Exception) {
RxPermissions(this) RxPermissions(this)
.request(Manifest.permission.CAMERA) .request(Manifest.permission.CAMERA)
.subscribe { .subscribe {
if (it) if (it)
if (forConfig) if (forConfig)
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java)) scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
else
scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
else else
scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java)) toast(R.string.toast_permission_denied)
else }
toast(R.string.toast_permission_denied)
}
// } // }
return true return true
} }
@@ -476,7 +368,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/** /**
* import config from clipboard * import config from clipboard
*/ */
private fun importClipboard() fun importClipboard()
: Boolean { : Boolean {
try { try {
val clipboard = Utils.getClipboard(this) val clipboard = Utils.getClipboard(this)
@@ -488,32 +380,27 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
return true return true
} }
private fun importBatchConfig(server: String?) { fun importBatchConfig(server: String?, subid: String = "") {
// val dialog = AlertDialog.Builder(this) val subid2 = if(subid.isNullOrEmpty()){
// .setView(LayoutProgressBinding.inflate(layoutInflater).root) mainViewModel.subscriptionId
// .setCancelable(false) }else{
// .show() subid
binding.pbWaiting.show() }
val append = subid.isNullOrEmpty()
lifecycleScope.launch(Dispatchers.IO) { var count = AngConfigManager.importBatchConfig(server, subid2, append)
val (count, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true) if (count <= 0) {
delay(500L) count = AngConfigManager.importBatchConfig(Utils.decode(server!!), subid2, append)
launch(Dispatchers.Main) { }
if (count > 0) { if (count > 0) {
toast(R.string.toast_success) toast(R.string.toast_success)
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
} else if (countSub > 0) { } else {
initGroupTab() toast(R.string.toast_failure)
} else {
toast(R.string.toast_failure)
}
//dialog.dismiss()
binding.pbWaiting.hide()
}
} }
} }
private fun importConfigCustomClipboard() fun importConfigCustomClipboard()
: Boolean { : Boolean {
try { try {
val configText = Utils.getClipboard(this) val configText = Utils.getClipboard(this)
@@ -532,7 +419,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/** /**
* import config from local config file * import config from local config file
*/ */
private fun importConfigCustomLocal(): Boolean { fun importConfigCustomLocal(): Boolean {
try { try {
showFileChooser() showFileChooser()
} catch (e: Exception) { } catch (e: Exception) {
@@ -542,7 +429,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
return true return true
} }
private fun importConfigCustomUrlClipboard() fun importConfigCustomUrlClipboard()
: Boolean { : Boolean {
try { try {
val url = Utils.getClipboard(this) val url = Utils.getClipboard(this)
@@ -560,7 +447,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/** /**
* import config from url * import config from url
*/ */
private fun importConfigCustomUrl(url: String?): Boolean { 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)
@@ -587,26 +474,43 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/** /**
* import config from sub * import config from sub
*/ */
private fun importConfigViaSub(): Boolean { fun importConfigViaSub()
// val dialog = AlertDialog.Builder(this) : Boolean {
// .setView(LayoutProgressBinding.inflate(layoutInflater).root) try {
// .setCancelable(false) toast(R.string.title_sub_update)
// .show() MmkvManager.decodeSubscriptions().forEach {
binding.pbWaiting.show() if (TextUtils.isEmpty(it.first)
|| TextUtils.isEmpty(it.second.remarks)
lifecycleScope.launch(Dispatchers.IO) { || TextUtils.isEmpty(it.second.url)
val count = mainViewModel.updateConfigViaSubAll() ) {
delay(500L) return@forEach
launch(Dispatchers.Main) { }
if (count > 0) { if (!it.second.enabled) {
toast(R.string.toast_success) return@forEach
mainViewModel.reloadServerList() }
} else { val url = it.second.url
toast(R.string.toast_failure) if (!Utils.isValidUrl(url)) {
return@forEach
}
Log.d(ANG_PACKAGE, url)
lifecycleScope.launch(Dispatchers.IO) {
val configText = try {
Utils.getUrlContentWithCustomUserAgent(url)
} catch (e: Exception) {
e.printStackTrace()
launch(Dispatchers.Main) {
toast("\"" + it.second.remarks + "\" " + getString(R.string.toast_failure))
}
return@launch
}
launch(Dispatchers.Main) {
importBatchConfig(configText, it.first)
}
} }
//dialog.dismiss()
binding.pbWaiting.hide()
} }
} catch (e: Exception) {
e.printStackTrace()
return false
} }
return true return true
} }
@@ -637,42 +541,34 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
* read content from uri * read content from uri
*/ */
private fun readContentFromUri(uri: Uri) { private fun readContentFromUri(uri: Uri) {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
RxPermissions(this) RxPermissions(this)
.request(permission) .request(Manifest.permission.READ_EXTERNAL_STORAGE)
.subscribe { .subscribe {
if (it) { if (it) {
try { try {
contentResolver.openInputStream(uri).use { input -> contentResolver.openInputStream(uri).use { input ->
importCustomizeConfig(input?.bufferedReader()?.readText()) importCustomizeConfig(input?.bufferedReader()?.readText())
}
} catch (e: Exception) {
e.printStackTrace()
} }
} catch (e: Exception) { } else
e.printStackTrace() toast(R.string.toast_permission_denied)
} }
} else
toast(R.string.toast_permission_denied)
}
} }
/** /**
* import customize config * import customize config
*/ */
private fun importCustomizeConfig(server: String?) { 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
} }
if (mainViewModel.appendCustomConfigServer(server)) { 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()
@@ -681,7 +577,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
} }
private fun setTestState(content: String?) { fun setTestState(content: String?) {
binding.tvTestState.text = content binding.tvTestState.text = content
} }
@@ -695,44 +591,66 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
// } // }
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) { if (keyCode == KeyEvent.KEYCODE_BACK) {
moveTaskToBack(false) moveTaskToBack(false)
return true return true
} }
return super.onKeyDown(keyCode, event) return super.onKeyDown(keyCode, event)
} }
fun showCircle() {
binding.fabProgressCircle.show()
}
fun hideCircle() {
try {
Observable.timer(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
try {
if (binding.fabProgressCircle.isShown) {
binding.fabProgressCircle.hide()
}
} catch (e: Exception) {
Log.w(ANG_PACKAGE, e)
}
}
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
}
}
override fun onBackPressed() {
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
binding.drawerLayout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
override fun onNavigationItemSelected(item: MenuItem): Boolean { 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 -> {
requestSubSettingActivity.launch(Intent(this, SubSettingActivity::class.java)) startActivity(Intent(this, SubSettingActivity::class.java))
} }
R.id.settings -> { R.id.settings -> {
startActivity( startActivity(Intent(this, SettingsActivity::class.java)
Intent(this, SettingsActivity::class.java) .putExtra("isRunning", mainViewModel.isRunning.value == true))
.putExtra("isRunning", mainViewModel.isRunning.value == true)
)
} }
R.id.user_asset_setting -> {
R.id.routing_setting -> { startActivity(Intent(this, UserAssetActivity::class.java))
requestSubSettingActivity.launch(Intent(this, RoutingSettingActivity::class.java)) }
R.id.feedback -> {
Utils.openUri(this, AppConfig.v2rayNGIssues)
} }
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 -> {
startActivity(Intent(this, AboutActivity::class.java))
}
} }
binding.drawerLayout.closeDrawer(GravityCompat.START) binding.drawerLayout.closeDrawer(GravityCompat.START)
return true return true
@@ -3,38 +3,43 @@ package com.v2ray.ang.ui
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.text.TextUtils import android.text.TextUtils
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import com.google.gson.Gson
import androidx.recyclerview.widget.RecyclerView import com.tencent.mmkv.MMKV
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
import com.v2ray.ang.databinding.ItemQrcodeBinding import com.v2ray.ang.databinding.ItemQrcodeBinding
import com.v2ray.ang.databinding.ItemRecyclerFooterBinding import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
import com.v2ray.ang.databinding.ItemRecyclerMainBinding import com.v2ray.ang.databinding.ItemRecyclerMainBinding
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.helper.ItemTouchHelperAdapter import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder import com.v2ray.ang.helper.ItemTouchHelperViewHolder
import com.v2ray.ang.service.V2RayServiceManager 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.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import rx.Observable
import io.reactivex.rxjava3.core.Observable import rx.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter { class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>()
, 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)
} }
@@ -45,7 +50,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 profile = mActivity.mainViewModel.serversCache[position].profile val config = mActivity.mainViewModel.serversCache[position].config
// //filter // //filter
// if (mActivity.mainViewModel.subscriptionId.isNotEmpty() // if (mActivity.mainViewModel.subscriptionId.isNotEmpty()
// && mActivity.mainViewModel.subscriptionId != config.subscriptionId // && mActivity.mainViewModel.subscriptionId != config.subscriptionId
@@ -55,53 +60,50 @@ 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 = profile.remarks holder.itemMainBinding.tvName.text = config.remarks
holder.itemView.setBackgroundColor(Color.TRANSPARENT) holder.itemView.setBackgroundColor(Color.TRANSPARENT)
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty() holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString() ?: ""
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 == MmkvManager.getSelectServer()) { if (guid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent) holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorSelected)
} else { } else {
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0) holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorUnselected)
}
holder.itemMainBinding.tvSubscription.text = ""
val json = subStorage?.decodeString(config.subscriptionId)
if (!json.isNullOrBlank()) {
val sub = Gson().fromJson(json, SubscriptionItem::class.java)
holder.itemMainBinding.tvSubscription.text = sub.remarks
} }
holder.itemMainBinding.tvSubscription.text = MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks ?: ""
var shareOptions = share_method.asList() var shareOptions = share_method.asList()
when (profile.configType) { when (config.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 -> {
holder.itemMainBinding.tvType.text = config.configType.name
}
else -> { else -> {
holder.itemMainBinding.tvType.text = profile.configType.name holder.itemMainBinding.tvType.text = config.configType.name.lowercase()
} }
} }
holder.itemMainBinding.tvStatistics.text = "${outbound?.getServerAddress()} : ${outbound?.getServerPort()}"
// 隐藏主页服务器地址为xxx:xxx:***/xxx.xxx.xxx.***
val strState = "${
profile.server?.let {
if (it.contains(":"))
it.split(":").take(2).joinToString(":", postfix = ":***")
else
it.split('.').dropLast(1).joinToString(".", postfix = ".***")
}
} : ${profile.serverPort}"
holder.itemMainBinding.tvStatistics.text = strState
holder.itemMainBinding.layoutShare.setOnClickListener { holder.itemMainBinding.layoutShare.setOnClickListener {
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i -> AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
try { try {
when (i) { when (i) {
0 -> { 0 -> {
if (profile.configType == EConfigType.CUSTOM) { if (config.configType == EConfigType.CUSTOM) {
shareFullContent(guid) shareFullContent(guid)
} else { } else {
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity)) val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
@@ -109,7 +111,6 @@ 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)
@@ -117,7 +118,6 @@ 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")
} }
@@ -129,47 +129,44 @@ 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 (profile.configType == EConfigType.CUSTOM) { if (config.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 != MmkvManager.getSelectServer()) { if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { if (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) { _, _ ->
//do noting
}
.show() .show()
} else { } else {
removeServer(guid, position) removeServer(guid, position)
} }
} else {
application.toast(R.string.toast_action_not_allowed)
} }
} }
holder.itemMainBinding.infoContainer.setOnClickListener { holder.itemMainBinding.infoContainer.setOnClickListener {
val selected = MmkvManager.getSelectServer() val selected = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
if (guid != selected) { if (guid != selected) {
MmkvManager.setSelectServer(guid) mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
if (!TextUtils.isEmpty(selected)) { if (!TextUtils.isEmpty(selected)) {
notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty())) notifyItemChanged(mActivity.mainViewModel.getPosition(selected!!))
} }
notifyItemChanged(mActivity.mainViewModel.getPosition(guid)) notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
if (isRunning) { if (isRunning) {
mActivity.showCircle()
Utils.stopVService(mActivity) Utils.stopVService(mActivity)
Observable.timer(500, TimeUnit.MILLISECONDS) Observable.timer(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe {
V2RayServiceManager.startV2Ray(mActivity) V2RayServiceManager.startV2Ray(mActivity)
} mActivity.hideCircle()
}
} }
} }
} }
@@ -180,7 +177,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
} else { } else {
holder.itemFooterBinding.layoutEdit.setOnClickListener { holder.itemFooterBinding.layoutEdit.setOnClickListener {
Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}") Utils.openUri(mActivity, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}")
} }
} }
} }
@@ -194,7 +191,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)
@@ -204,7 +201,6 @@ 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))
} }
@@ -229,21 +225,36 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
} }
class MainViewHolder(val itemMainBinding: ItemRecyclerMainBinding) : class MainViewHolder(val itemMainBinding: ItemRecyclerMainBinding) :
BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder
class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) : class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
BaseViewHolder(itemFooterBinding.root) BaseViewHolder(itemFooterBinding.root), ItemTouchHelperViewHolder
override fun onItemDismiss(position: Int) {
val guid = mActivity.mainViewModel.serversCache.getOrNull(position)?.guid ?: return
if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
// mActivity.alert(R.string.del_config_comfirm) {
// positiveButton(android.R.string.ok) {
mActivity.mainViewModel.removeServer(guid)
notifyItemRemoved(position)
// }
// show()
// }
}
}
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
mActivity.mainViewModel.swapServer(fromPosition, toPosition) mActivity.mainViewModel.swapServer(fromPosition, toPosition)
notifyItemMoved(fromPosition, toPosition) notifyItemMoved(fromPosition, toPosition)
// position is changed, since position is used by click callbacks, need to update range
if (toPosition > fromPosition)
notifyItemRangeChanged(fromPosition, toPosition - fromPosition + 1)
else
notifyItemRangeChanged(toPosition, fromPosition - toPosition + 1)
return true return true
} }
override fun onItemMoveCompleted() { override fun onItemMoveCompleted() {
// do nothing // do nothing
} }
override fun onItemDismiss(position: Int) {
}
} }
@@ -8,6 +8,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
@@ -18,58 +19,62 @@ import com.v2ray.ang.dto.AppInfo
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.v2RayApplication import com.v2ray.ang.extension.v2RayApplication
import com.v2ray.ang.util.AppManagerUtil import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.MmkvManager.settingsStorage
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.GlobalScope
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
import java.util.*
class PerAppProxyActivity : BaseActivity() { class PerAppProxyActivity : BaseActivity() {
private val binding by lazy { private lateinit var binding: ActivityBypassListBinding
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
private val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) binding = ActivityBypassListBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL) val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
binding.recyclerView.addItemDecoration(dividerItemDecoration) binding.recyclerView.addItemDecoration(dividerItemDecoration)
val blacklist = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET) val blacklist = defaultSharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, null)
AppManagerUtil.rxLoadNetworkAppList(this) AppManagerUtil.rxLoadNetworkAppList(this)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.map { .map {
if (blacklist != null) { if (blacklist != null) {
it.forEach { one -> it.forEach { one ->
if (blacklist.contains(one.packageName)) { if ((blacklist.contains(one.packageName))) {
one.isSelected = 1 one.isSelected = 1
} else { } else {
one.isSelected = 0 one.isSelected = 0
}
} }
} val comparator = Comparator<AppInfo> { p1, p2 ->
val comparator = Comparator<AppInfo> { p1, p2 -> when {
when { p1.isSelected > p2.isSelected -> -1
p1.isSelected > p2.isSelected -> -1 p1.isSelected == p2.isSelected -> 0
p1.isSelected == p2.isSelected -> 0 else -> 1
else -> 1 }
} }
it.sortedWith(comparator)
} else {
val comparator = object : Comparator<AppInfo> {
val collator = Collator.getInstance()
override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
}
it.sortedWith(comparator)
} }
it.sortedWith(comparator)
} else {
val comparator = object : Comparator<AppInfo> {
val collator = Collator.getInstance()
override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
}
it.sortedWith(comparator)
} }
}
// .map { // .map {
// val comparator = object : Comparator<AppInfo> { // val comparator = object : Comparator<AppInfo> {
// val collator = Collator.getInstance() // val collator = Collator.getInstance()
@@ -77,13 +82,13 @@ class PerAppProxyActivity : BaseActivity() {
// } // }
// it.sortedWith(comparator) // it.sortedWith(comparator)
// } // }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe {
appsAll = it appsAll = it
adapter = PerAppProxyAdapter(this, it, blacklist) adapter = PerAppProxyAdapter(this, it, blacklist)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
binding.pbWaiting.visibility = View.GONE binding.pbWaiting.visibility = View.GONE
} }
/*** /***
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() { recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
var dst = 0 var dst = 0
@@ -132,14 +137,14 @@ class PerAppProxyActivity : BaseActivity() {
***/ ***/
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked -> binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
settingsStorage.encode(AppConfig.PREF_PER_APP_PROXY, isChecked) defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_PER_APP_PROXY, isChecked).apply()
} }
binding.switchPerAppProxy.isChecked = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false) binding.switchPerAppProxy.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked -> binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked ->
settingsStorage.encode(AppConfig.PREF_BYPASS_APPS, isChecked) defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_BYPASS_APPS, isChecked).apply()
} }
binding.switchBypassApps.isChecked = settingsStorage.getBoolean(AppConfig.PREF_BYPASS_APPS, false) binding.switchBypassApps.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_BYPASS_APPS, false)
/*** /***
et_search.setOnEditorActionListener { v, actionId, event -> et_search.setOnEditorActionListener { v, actionId, event ->
@@ -175,7 +180,7 @@ class PerAppProxyActivity : BaseActivity() {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
adapter?.let { adapter?.let {
settingsStorage.encode(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist) defaultSharedPreferences.edit().putStringSet(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist).apply()
} }
} }
@@ -186,10 +191,12 @@ 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 = false override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean { override fun onQueryTextChange(newText: String?): Boolean {
filterProxyApp(newText.orEmpty()) filterProxyApp(newText!!)
return false return false
} }
}) })
@@ -205,33 +212,29 @@ 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,7 +253,9 @@ class PerAppProxyActivity : BaseActivity() {
private fun importProxyApp() { private fun importProxyApp() {
val content = Utils.getClipboard(applicationContext) val content = Utils.getClipboard(applicationContext)
if (TextUtils.isEmpty(content)) return if (TextUtils.isEmpty(content)) {
return
}
selectProxyApp(content, false) selectProxyApp(content, false)
toast(R.string.toast_success) toast(R.string.toast_success)
} }
@@ -272,9 +277,11 @@ class PerAppProxyActivity : BaseActivity() {
} else { } else {
content content
} }
if (TextUtils.isEmpty(proxyApps)) return false if (TextUtils.isEmpty(proxyApps)) {
return false
}
adapter?.blacklist?.clear() adapter?.blacklist!!.clear()
if (binding.switchBypassApps.isChecked) { if (binding.switchBypassApps.isChecked) {
adapter?.let { adapter?.let {
@@ -282,7 +289,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
} }
@@ -295,7 +302,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
} }
@@ -312,8 +319,12 @@ 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") return false if (packageName == "com.google.android.webview") {
if (packageName.startsWith("com.google")) return true return false
}
if (packageName.startsWith("com.google")) {
return true
}
} }
return proxyApps.indexOf(packageName) >= 0 return proxyApps.indexOf(packageName) >= 0
@@ -326,8 +337,7 @@ 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)
} }
} }
@@ -1,15 +1,16 @@
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>() {
companion object { companion object {
private const val VIEW_TYPE_HEADER = 0 private const val VIEW_TYPE_HEADER = 0
@@ -33,10 +34,8 @@ 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( view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 0)
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
@@ -52,7 +51,7 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
inner class AppViewHolder(private val itemBypassBinding: ItemRecyclerBypassListBinding) : BaseViewHolder(itemBypassBinding.root), inner class AppViewHolder(private val itemBypassBinding: ItemRecyclerBypassListBinding) : BaseViewHolder(itemBypassBinding.root),
View.OnClickListener { View.OnClickListener {
private val inBlacklist: Boolean get() = blacklist.contains(appInfo.packageName) private val inBlacklist: Boolean get() = blacklist.contains(appInfo.packageName)
private lateinit var appInfo: AppInfo private lateinit var appInfo: AppInfo
@@ -1,126 +0,0 @@
package com.v2ray.ang.ui
import android.os.Bundle
import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingEditBinding
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.SettingsManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class RoutingEditActivity : BaseActivity() {
private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) }
private val position by lazy { intent.getIntExtra("position", -1) }
private val outbound_tag: Array<out String> by lazy {
resources.getStringArray(R.array.outbound_tag)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
title = getString(R.string.routing_settings_rule_title)
val rulesetItem = SettingsManager.getRoutingRuleset(position)
if (rulesetItem != null) {
bindingServer(rulesetItem)
} else {
clearServer()
}
}
private fun bindingServer(rulesetItem: RulesetItem): Boolean {
binding.etRemarks.text = Utils.getEditable(rulesetItem.remarks)
binding.chkLocked.isChecked = rulesetItem.looked ?: false
binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(","))
binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(","))
binding.etPort.text = Utils.getEditable(rulesetItem.port)
binding.etProtocol.text = Utils.getEditable(rulesetItem.protocol?.joinToString(","))
binding.etNetwork.text = Utils.getEditable(rulesetItem.network)
val outbound = Utils.arrayFind(outbound_tag, rulesetItem.outboundTag)
binding.spOutboundTag.setSelection(outbound)
return true
}
private fun clearServer(): Boolean {
binding.etRemarks.text = null
binding.spOutboundTag.setSelection(0)
return true
}
private fun saveServer(): Boolean {
val rulesetItem = SettingsManager.getRoutingRuleset(position) ?: RulesetItem()
rulesetItem.remarks = binding.etRemarks.text.toString()
rulesetItem.looked = binding.chkLocked.isChecked
binding.etDomain.text.toString().let { rulesetItem.domain = if (it.isEmpty()) null else it.split(",").map { itt -> itt.trim() }.filter { itt -> itt.isNotEmpty() } }
binding.etIp.text.toString().let { rulesetItem.ip = if (it.isEmpty()) null else it.split(",").map { itt -> itt.trim() }.filter { itt -> itt.isNotEmpty() } }
binding.etProtocol.text.toString().let { rulesetItem.protocol = if (it.isEmpty()) null else it.split(",").map { itt -> itt.trim() }.filter { itt -> itt.isNotEmpty() } }
binding.etPort.text.toString().let { rulesetItem.port = it.ifEmpty { null } }
binding.etNetwork.text.toString().let { rulesetItem.network = it.ifEmpty { null } }
rulesetItem.outboundTag = outbound_tag[binding.spOutboundTag.selectedItemPosition]
if (TextUtils.isEmpty(rulesetItem.remarks)) {
toast(R.string.sub_setting_remarks)
return false
}
SettingsManager.saveRoutingRuleset(position, rulesetItem)
toast(R.string.toast_success)
finish()
return true
}
private fun deleteServer(): Boolean {
if (position >= 0) {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch(Dispatchers.IO) {
SettingsManager.removeRoutingRuleset(position)
launch(Dispatchers.Main) {
finish()
}
}
}
.setNegativeButton(android.R.string.no) { _, _ ->
// do nothing
}
.show()
}
return true
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.action_server, menu)
val del_config = menu.findItem(R.id.del_config)
if (position < 0) {
del_config?.isVisible = false
}
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.del_config -> {
deleteServer()
true
}
R.id.save_config -> {
saveServer()
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -1,158 +0,0 @@
package com.v2ray.ang.ui
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.SettingsManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class RoutingSettingActivity : BaseActivity() {
private val binding by lazy { ActivityRoutingSettingBinding.inflate(layoutInflater) }
var rulesets: MutableList<RulesetItem> = mutableListOf()
private val adapter by lazy { RoutingSettingRecyclerAdapter(this) }
private var mItemTouchHelper: ItemTouchHelper? = null
private val routing_domain_strategy: Array<out String> by lazy {
resources.getStringArray(R.array.routing_domain_strategy)
}
private val preset_rulesets: Array<out String> by lazy {
resources.getStringArray(R.array.preset_rulesets)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
title = getString(R.string.routing_settings_title)
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
val found = Utils.arrayFind(routing_domain_strategy, settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "")
found.let { binding.spDomainStrategy.setSelection(if (it >= 0) it else 0) }
binding.spDomainStrategy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
settingsStorage.encode(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, routing_domain_strategy[position])
}
}
}
override fun onResume() {
super.onResume()
refreshData()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_routing_setting, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.add_rule -> {
startActivity(Intent(this, RoutingEditActivity::class.java))
true
}
R.id.user_asset_setting -> {
startActivity(Intent(this, UserAssetActivity::class.java))
true
}
R.id.import_rulesets -> {
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
.setPositiveButton(android.R.string.ok) { _, _ ->
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
try {
lifecycleScope.launch(Dispatchers.IO) {
SettingsManager.resetRoutingRulesets(this@RoutingSettingActivity, i)
launch(Dispatchers.Main) {
refreshData()
toast(R.string.toast_success)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}.show()
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting
}
.show()
true
}
R.id.import_rulesets_from_clipboard -> {
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
.setPositiveButton(android.R.string.ok) { _, _ ->
try {
val clipboard = Utils.getClipboard(this)
lifecycleScope.launch(Dispatchers.IO) {
val ret = SettingsManager.resetRoutingRulesetsFromClipboard(clipboard)
launch(Dispatchers.Main) {
if (ret) {
refreshData()
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting
}
.show()
true
}
R.id.export_rulesets_to_clipboard -> {
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) {
toast(R.string.toast_failure)
} else {
Utils.setClipboard(this, JsonUtil.toJson(rulesetList))
toast(R.string.toast_success)
}
true
}
else -> super.onOptionsItemSelected(item)
}
fun refreshData() {
rulesets = MmkvManager.decodeRoutingRulesets() ?: mutableListOf()
adapter.notifyDataSetChanged()
}
}
@@ -1,81 +0,0 @@
package com.v2ray.ang.ui
import android.content.Intent
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.databinding.ItemRecyclerRoutingSettingBinding
import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
import com.v2ray.ang.ui.MainRecyclerAdapter.BaseViewHolder
import com.v2ray.ang.util.SettingsManager
class RoutingSettingRecyclerAdapter(val activity: RoutingSettingActivity) : RecyclerView.Adapter<RoutingSettingRecyclerAdapter.MainViewHolder>(),
ItemTouchHelperAdapter {
private var mActivity: RoutingSettingActivity = activity
override fun getItemCount() = mActivity.rulesets.size
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
val ruleset = mActivity.rulesets[position]
holder.itemRoutingSettingBinding.remarks.text = ruleset.remarks
holder.itemRoutingSettingBinding.domainIp.text = (ruleset.domain ?: ruleset.ip ?: ruleset.port)?.toString()
holder.itemRoutingSettingBinding.outboundTag.text = ruleset.outboundTag
holder.itemRoutingSettingBinding.chkEnable.isChecked = ruleset.enabled
holder.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.looked ?: false
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener {
mActivity.startActivity(
Intent(mActivity, RoutingEditActivity::class.java)
.putExtra("position", position)
)
}
holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
if( !it.isPressed) return@setOnCheckedChangeListener
ruleset.enabled = isChecked
SettingsManager.saveRoutingRuleset(position, ruleset)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
return MainViewHolder(
ItemRecyclerRoutingSettingBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
class MainViewHolder(val itemRoutingSettingBinding: ItemRecyclerRoutingSettingBinding) :
BaseViewHolder(itemRoutingSettingBinding.root), ItemTouchHelperViewHolder
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun onItemSelected() {
itemView.setBackgroundColor(Color.LTGRAY)
}
fun onItemClear() {
itemView.setBackgroundColor(0)
}
}
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
SettingsManager.swapRoutingRuleset(fromPosition, toPosition)
notifyItemMoved(fromPosition, toPosition)
return true
}
override fun onItemMoveCompleted() {
mActivity.refreshData()
}
override fun onItemDismiss(position: Int) {
}
}
@@ -0,0 +1,38 @@
package com.v2ray.ang.ui
import android.os.Bundle
import com.v2ray.ang.R
import androidx.fragment.app.Fragment
import com.google.android.material.tabs.TabLayoutMediator
import com.v2ray.ang.AppConfig
import com.v2ray.ang.databinding.ActivityRoutingSettingsBinding
class RoutingSettingsActivity : BaseActivity() {
private lateinit var binding: ActivityRoutingSettingsBinding
private val titles: Array<out String> by lazy {
resources.getStringArray(R.array.routing_tag)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityRoutingSettingsBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
title = getString(R.string.title_pref_routing_custom)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val fragments = ArrayList<Fragment>()
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_AGENT))
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_DIRECT))
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_BLOCKED))
val adapter = FragmentAdapter(this, fragments)
binding.viewpager.adapter = adapter
//tablayout.setTabTextColors(Color.BLACK, Color.RED)
TabLayoutMediator(binding.tablayout, binding.viewpager) { tab, position ->
tab.text = titles[position]
}.attach()
}
}
@@ -0,0 +1,158 @@
package com.v2ray.ang.ui
import android.Manifest
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.view.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.tbruyelle.rxpermissions.RxPermissions
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.FragmentRoutingSettingsBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.v2RayApplication
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class RoutingSettingsFragment : Fragment() {
private lateinit var binding: FragmentRoutingSettingsBinding
companion object {
private const val routing_arg = "routing_arg"
}
val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
binding = FragmentRoutingSettingsBinding.inflate(layoutInflater)
return binding.root// inflater.inflate(R.layout.fragment_routing_settings, container, false)
}
fun newInstance(arg: String): Fragment {
val fragment = RoutingSettingsFragment()
val bundle = Bundle()
bundle.putString(routing_arg, arg)
fragment.arguments = bundle
return fragment
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val content = defaultSharedPreferences.getString(requireArguments().getString(routing_arg), "")
binding.etRoutingContent.text = Utils.getEditable(content!!)
setHasOptionsMenu(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_routing, menu)
return super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.save_routing -> {
saveRouting()
true
}
R.id.del_routing -> {
binding.etRoutingContent.text = null
true
}
R.id.scan_replace -> {
scanQRcode(true)
true
}
R.id.scan_append -> {
scanQRcode(false)
true
}
R.id.default_rules -> {
setDefaultRules()
true
}
else -> super.onOptionsItemSelected(item)
}
private fun saveRouting() {
val content = binding.etRoutingContent.text.toString()
defaultSharedPreferences.edit().putString(requireArguments().getString(routing_arg), content).apply()
activity?.toast(R.string.toast_success)
}
fun scanQRcode(forReplace: Boolean): Boolean {
// try {
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
// .addCategory(Intent.CATEGORY_DEFAULT)
// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
// } catch (e: Exception) {
RxPermissions(requireActivity())
.request(Manifest.permission.CAMERA)
.subscribe {
if (it)
if (forReplace)
scanQRCodeForReplace.launch(Intent(activity, ScannerActivity::class.java))
else
scanQRCodeForAppend.launch(Intent(activity, ScannerActivity::class.java))
else
activity?.toast(R.string.toast_permission_denied)
}
// }
return true
}
private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
val content = it.data?.getStringExtra("SCAN_RESULT")
binding.etRoutingContent.text = Utils.getEditable(content!!)
}
}
private val scanQRCodeForAppend = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
val content = it.data?.getStringExtra("SCAN_RESULT")
binding.etRoutingContent.text = Utils.getEditable("${binding.etRoutingContent.text},$content")
}
}
fun setDefaultRules(): Boolean {
var url = AppConfig.v2rayCustomRoutingListUrl
var tag = ""
when (requireArguments().getString(routing_arg)) {
AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
tag = AppConfig.TAG_AGENT
}
AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
tag = AppConfig.TAG_DIRECT
}
AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> {
tag = AppConfig.TAG_BLOCKED
}
}
url += tag
activity?.toast(R.string.msg_downloading_content)
lifecycleScope.launch(Dispatchers.IO) {
val content = Utils.getUrlContext(url, 5000)
launch(Dispatchers.Main) {
val routingList = if (TextUtils.isEmpty(content)) {
Utils.readTextFromAssets(activity?.v2RayApplication!!, "custom_routing_$tag")
} else {
content
}
binding.etRoutingContent.text = Utils.getEditable(routingList)
saveRouting()
//toast(R.string.toast_success)
}
}
return true
}
}
@@ -1,13 +1,13 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.Manifest import android.Manifest
import android.content.Intent import android.content.*
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() {
@@ -19,21 +19,21 @@ class ScScannerActivity : BaseActivity() {
fun importQRcode(): Boolean { fun importQRcode(): Boolean {
RxPermissions(this) RxPermissions(this)
.request(Manifest.permission.CAMERA) .request(Manifest.permission.CAMERA)
.subscribe { .subscribe {
if (it) if (it)
scanQRCode.launch(Intent(this, ScannerActivity::class.java)) scanQRCode.launch(Intent(this, ScannerActivity::class.java))
else else
toast(R.string.toast_permission_denied) toast(R.string.toast_permission_denied)
} }
return true return true
} }
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) { if (it.resultCode == RESULT_OK) {
val (count, countSub) = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false) val count = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false)
if (count + countSub > 0) { if (count > 0) {
toast(R.string.toast_success) toast(R.string.toast_success)
} else { } else {
toast(R.string.toast_failure) toast(R.string.toast_failure)
@@ -1,9 +1,9 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.os.Bundle
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import android.os.Bundle
import com.v2ray.ang.service.V2RayServiceManager
class ScSwitchActivity : BaseActivity() { class ScSwitchActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -1,57 +1,65 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.Manifest import android.Manifest
import android.app.Activity
import android.os.Bundle
import com.google.zxing.Result
import me.dm7.barcodescanner.zxing.ZXingScannerView
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
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.rxpermissions3.RxPermissions import com.google.zxing.BarcodeFormat
import com.v2ray.ang.AppConfig import com.tbruyelle.rxpermissions.RxPermissions
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.QRCodeDecoder import com.v2ray.ang.util.QRCodeDecoder
import io.github.g00fy2.quickie.QRResult
import io.github.g00fy2.quickie.ScanCustomCode
import io.github.g00fy2.quickie.config.ScannerConfig
class ScannerActivity : BaseActivity() { class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult) private var mScannerView: ZXingScannerView? = null
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mScannerView = ZXingScannerView(this) // Programmatically initialize the scanner view
if (settingsStorage?.decodeBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) { mScannerView?.setAutoFocus(true)
launchScan() val formats = ArrayList<BarcodeFormat>()
} formats.add(BarcodeFormat.QR_CODE)
mScannerView?.setFormats(formats)
setContentView(mScannerView) // Set the scanner view as the content view
supportActionBar?.setDisplayHomeAsUpEnabled(true)
} }
private fun launchScan() { public override fun onResume() {
scanQrCode.launch( super.onResume()
ScannerConfig.build { mScannerView!!.setResultHandler(this) // Register ourselves as a handler for scan results.
setHapticSuccessFeedback(true) // enable (default) or disable haptic feedback when a barcode was detected mScannerView!!.startCamera() // Start camera on resume
setShowTorchToggle(true) // show or hide (default) torch/flashlight toggle button
setShowCloseButton(true) // show or hide (default) close button
}
)
} }
private fun handleResult(result: QRResult) { public override fun onPause() {
if (result is QRResult.QRSuccess) { super.onPause()
finished(result.content.rawValue.orEmpty()) mScannerView!!.stopCamera() // Stop camera on pause
} else { }
finish()
} override fun handleResult(rawResult: Result) {
// Do something with the result here
// Log.v(FragmentActivity.TAG, rawResult.text) // Prints scan results
// Log.v(FragmentActivity.TAG, rawResult.barcodeFormat.toString()) // Prints the scan format (qrcode, pdf417 etc.)
finished(rawResult.text)
// If you would like to resume scanning, call this method below:
// mScannerView!!.resumeCameraPreview(this)
} }
private fun finished(text: String) { private fun finished(text: String) {
val intent = Intent() val intent = Intent()
intent.putExtra("SCAN_RESULT", text) intent.putExtra("SCAN_RESULT", text)
setResult(RESULT_OK, intent) setResult(Activity.RESULT_OK, intent)
finish() finish()
} }
@@ -61,32 +69,21 @@ class ScannerActivity : BaseActivity() {
} }
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.scan_code -> {
launchScan()
true
}
R.id.select_photo -> { R.id.select_photo -> {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
RxPermissions(this) RxPermissions(this)
.request(permission) .request(Manifest.permission.READ_EXTERNAL_STORAGE)
.subscribe { .subscribe {
if (it) { if (it) {
try { try {
showFileChooser() showFileChooser()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} else } else
toast(R.string.toast_permission_denied) toast(R.string.toast_permission_denied)
} }
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
@@ -109,7 +106,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.orEmpty()) finished(text!!)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
toast(e.message.toString()) toast(e.message.toString())
@@ -5,42 +5,36 @@ 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.AdapterView import android.widget.*
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.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_V6
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_FLOW
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_PORT import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_PORT
import com.v2ray.ang.dto.V2rayConfig.Companion.TLS import com.v2ray.ang.dto.V2rayConfig.Companion.XTLS
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage import com.v2ray.ang.util.MmkvManager.ID_MAIN
import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.Utils.getIpv6Address
class ServerActivity : BaseActivity() { class ServerActivity : BaseActivity() {
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() } private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() }
private val isRunning by lazy { private val isRunning by lazy {
intent.getBooleanExtra("isRunning", false) intent.getBooleanExtra("isRunning", false)
&& editGuid.isNotEmpty() && editGuid.isNotEmpty()
&& editGuid == MmkvManager.getSelectServer() && editGuid == mainStorage?.decodeString(KEY_SELECTED_SERVER)
} }
private val createConfigType by lazy { private val createConfigType by lazy {
EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value)) EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value)) ?: EConfigType.VMESS
?: EConfigType.VMESS
} }
private val subscriptionId by lazy { private val subscriptionId by lazy {
intent.getStringExtra("subscriptionId") intent.getStringExtra("subscriptionId")
@@ -73,13 +67,9 @@ class ServerActivity : BaseActivity() {
private val allowinsecures: Array<out String> by lazy { private val allowinsecures: Array<out String> by lazy {
resources.getStringArray(R.array.allowinsecures) resources.getStringArray(R.array.allowinsecures)
} }
private val uTlsItems: Array<out String> by lazy { private val uTlsItems: Array<out String> by lazy {
resources.getStringArray(R.array.streamsecurity_utls) resources.getStringArray(R.array.streamsecurity_utls)
} }
private val alpns: Array<out String> by lazy {
resources.getStringArray(R.array.streamsecurity_alpn)
}
// Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach. // Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
// We don't use AndroidViewBinding because, it is better to share similar logics for different // We don't use AndroidViewBinding because, it is better to share similar logics for different
// protocols. Use findViewById manually ensures the xml are de-coupled with the activity logic. // protocols. Use findViewById manually ensures the xml are de-coupled with the activity logic.
@@ -93,61 +83,32 @@ class ServerActivity : BaseActivity() {
private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) } private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) }
private val sp_stream_security: Spinner? by lazy { findViewById(R.id.sp_stream_security) } private val sp_stream_security: Spinner? by lazy { findViewById(R.id.sp_stream_security) }
private val sp_allow_insecure: Spinner? by lazy { findViewById(R.id.sp_allow_insecure) } private val sp_allow_insecure: Spinner? by lazy { findViewById(R.id.sp_allow_insecure) }
private val container_allow_insecure: LinearLayout? by lazy { findViewById(R.id.lay_allow_insecure) }
private val et_sni: EditText? by lazy { findViewById(R.id.et_sni) } private val et_sni: EditText? by lazy { findViewById(R.id.et_sni) }
private val container_sni: LinearLayout? by lazy { findViewById(R.id.lay_sni) }
private val sp_stream_fingerprint: Spinner? by lazy { findViewById(R.id.sp_stream_fingerprint) } //uTLS private val sp_stream_fingerprint: Spinner? by lazy { findViewById(R.id.sp_stream_fingerprint) } //uTLS
private val container_fingerprint: LinearLayout? by lazy { findViewById(R.id.lay_stream_fingerprint) }
private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) } private val sp_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 container_alpn: LinearLayout? by lazy { findViewById(R.id.lay_stream_alpn) }
private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) }
private val container_public_key: LinearLayout? by lazy { findViewById(R.id.lay_public_key) }
private val et_short_id: EditText? by lazy { findViewById(R.id.et_short_id) }
private val container_short_id: LinearLayout? by lazy { findViewById(R.id.lay_short_id) }
private val et_spider_x: EditText? by lazy { findViewById(R.id.et_spider_x) }
private val container_spider_x: LinearLayout? by lazy { findViewById(R.id.lay_spider_x) }
private val et_reserved1: EditText? by lazy { findViewById(R.id.et_reserved1) }
private val et_reserved2: EditText? by lazy { findViewById(R.id.et_reserved2) }
private val et_reserved3: EditText? by lazy { findViewById(R.id.et_reserved3) }
private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) }
private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) }
private val et_obfs_password: EditText? by lazy { findViewById(R.id.et_obfs_password) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
title = getString(R.string.title_server) title = getString(R.string.title_server)
val config = MmkvManager.decodeServerConfig(editGuid) val config = MmkvManager.decodeServerConfig(editGuid)
when (config?.configType ?: createConfigType) { when(config?.configType ?: createConfigType) {
EConfigType.VMESS -> setContentView(R.layout.activity_server_vmess) EConfigType.VMESS -> setContentView(R.layout.activity_server_vmess)
EConfigType.CUSTOM -> return EConfigType.CUSTOM -> return
EConfigType.SHADOWSOCKS -> setContentView(R.layout.activity_server_shadowsocks) EConfigType.SHADOWSOCKS -> setContentView(R.layout.activity_server_shadowsocks)
EConfigType.SOCKS -> setContentView(R.layout.activity_server_socks) EConfigType.SOCKS -> setContentView(R.layout.activity_server_socks)
EConfigType.HTTP -> setContentView(R.layout.activity_server_socks)
EConfigType.VLESS -> setContentView(R.layout.activity_server_vless) EConfigType.VLESS -> setContentView(R.layout.activity_server_vless)
EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan) EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan)
EConfigType.WIREGUARD -> setContentView(R.layout.activity_server_wireguard)
EConfigType.HYSTERIA2 -> setContentView(R.layout.activity_server_hysteria2)
} }
sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected( override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
val types = transportTypes(networks[position]) val types = transportTypes(networks[position])
sp_header_type?.isEnabled = types.size > 1 sp_header_type?.isEnabled = types.size > 1
val adapter = val adapter = ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
sp_header_type?.adapter = adapter sp_header_type?.adapter = adapter
sp_header_type_title?.text = if (networks[position] == "grpc") sp_header_type_title?.text = if (networks[position] == "grpc")
@@ -158,201 +119,66 @@ 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<*>?) {
// do nothing // do nothing
} }
} }
sp_stream_security?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
if (streamSecuritys[position].isBlank()) {
container_sni?.visibility = View.GONE
container_fingerprint?.visibility = View.GONE
container_alpn?.visibility = View.GONE
container_allow_insecure?.visibility = View.GONE
container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE
} else {
container_sni?.visibility = View.VISIBLE
container_fingerprint?.visibility = View.VISIBLE
container_alpn?.visibility = View.VISIBLE
if (streamSecuritys[position] == TLS) {
container_allow_insecure?.visibility = View.VISIBLE
container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE
} else {
container_allow_insecure?.visibility = View.GONE
container_alpn?.visibility = View.GONE
container_public_key?.visibility = View.VISIBLE
container_short_id?.visibility = View.VISIBLE
container_spider_x?.visibility = View.VISIBLE
}
}
}
override fun onNothingSelected(p0: AdapterView<*>?) {
// do nothing
}
}
if (config != null) { if (config != null) {
bindingServer(config) bindingServer(config)
} else { } else {
clearServer() clearServer()
} }
supportActionBar?.setDisplayHomeAsUpEnabled(true)
} }
/** /**
* binding selected server config * bingding seleced server config
*/ */
private fun bindingServer(config: ServerConfig): Boolean { private fun bindingServer(config: ServerConfig): Boolean {
val outbound = config.getProxyOutbound() ?: return false val outbound = config.getProxyOutbound() ?: return false
val streamSetting = config.outboundBean?.streamSettings ?: return false
et_remarks.text = Utils.getEditable(config.remarks) et_remarks.text = Utils.getEditable(config.remarks)
et_address.text = Utils.getEditable(outbound.getServerAddress().orEmpty()) et_address.text = Utils.getEditable(outbound.getServerAddress().orEmpty())
et_port.text = et_port.text = Utils.getEditable(outbound.getServerPort()?.toString() ?: DEFAULT_PORT.toString())
Utils.getEditable(outbound.getServerPort()?.toString() ?: DEFAULT_PORT.toString())
et_id.text = Utils.getEditable(outbound.getPassword().orEmpty()) et_id.text = Utils.getEditable(outbound.getPassword().orEmpty())
et_alterId?.text = et_alterId?.text = Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString())
Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()) if (config.configType == EConfigType.SOCKS) {
if (config.configType == EConfigType.SOCKS et_security?.text = Utils.getEditable(outbound.settings?.servers?.get(0)?.users?.get(0)?.user.orEmpty())
|| config.configType == EConfigType.HTTP
) {
et_security?.text =
Utils.getEditable(outbound.settings?.servers?.get(0)?.users?.get(0)?.user.orEmpty())
} else if (config.configType == EConfigType.VLESS) { } else if (config.configType == EConfigType.VLESS) {
et_security?.text = Utils.getEditable(outbound.getSecurityEncryption().orEmpty()) et_security?.text = Utils.getEditable(outbound.getSecurityEncryption().orEmpty())
val flow = Utils.arrayFind( val flow = Utils.arrayFind(flows, outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow.orEmpty())
flows,
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow.orEmpty()
)
if (flow >= 0) { if (flow >= 0) {
sp_flow?.setSelection(flow) sp_flow?.setSelection(flow)
} }
} else if (config.configType == EConfigType.WIREGUARD) { } else if (config.configType == EConfigType.TROJAN) {
et_public_key?.text = val flow = Utils.arrayFind(flows, outbound.settings?.servers?.get(0)?.flow.orEmpty())
Utils.getEditable(outbound.settings?.peers?.get(0)?.publicKey.orEmpty()) if (flow >= 0) {
if (outbound.settings?.reserved == null) { sp_flow?.setSelection(flow)
et_reserved1?.text = Utils.getEditable("0")
et_reserved2?.text = Utils.getEditable("0")
et_reserved3?.text = Utils.getEditable("0")
} else {
et_reserved1?.text =
Utils.getEditable(outbound.settings?.reserved?.get(0).toString())
et_reserved2?.text =
Utils.getEditable(outbound.settings?.reserved?.get(1).toString())
et_reserved3?.text =
Utils.getEditable(outbound.settings?.reserved?.get(2).toString())
} }
if (outbound.settings?.address == null) {
et_local_address?.text =
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
} else {
val list = outbound.settings?.address as List<*>
et_local_address?.text = Utils.getEditable(list.joinToString(","))
}
if (outbound.settings?.mtu == null) {
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
} else {
et_local_mtu?.text = Utils.getEditable(outbound.settings?.mtu.toString())
}
} else if (config.configType == EConfigType.HYSTERIA2) {
et_obfs_password?.text = Utils.getEditable(outbound.settings?.obfsPassword)
} }
val securityEncryptions = if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
val securityEncryptions = val security = Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty())
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
val security =
Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty())
if (security >= 0) { if (security >= 0) {
sp_security?.setSelection(security) sp_security?.setSelection(security)
} }
val streamSetting = config.outboundBean?.streamSettings ?: return true
val streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security) val streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security)
if (streamSecurity >= 0) { if (streamSecurity >= 0) {
sp_stream_security?.setSelection(streamSecurity) sp_stream_security?.setSelection(streamSecurity)
(streamSetting.tlsSettings ?: streamSetting.realitySettings)?.let { tlsSetting -> (streamSetting.tlsSettings?: streamSetting.xtlsSettings)?.let { tlsSetting ->
container_sni?.visibility = View.VISIBLE val allowinsecure = Utils.arrayFind(allowinsecures, tlsSetting.allowInsecure.toString())
container_fingerprint?.visibility = View.VISIBLE if (allowinsecure >= 0) {
container_alpn?.visibility = View.VISIBLE sp_allow_insecure?.setSelection(allowinsecure)
}
et_sni?.text = Utils.getEditable(tlsSetting.serverName) et_sni?.text = Utils.getEditable(tlsSetting.serverName)
tlsSetting.fingerprint?.let { tlsSetting.fingerprint?.let {
val utlsIndex = Utils.arrayFind(uTlsItems, tlsSetting.fingerprint) val utlsIndex = Utils.arrayFind(uTlsItems, tlsSetting.fingerprint)
sp_stream_fingerprint?.setSelection(utlsIndex) sp_stream_fingerprint?.setSelection(utlsIndex)
} }
tlsSetting.alpn?.let {
val alpnIndex = Utils.arrayFind(
alpns,
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
)
sp_stream_alpn?.setSelection(alpnIndex)
}
if (streamSetting.tlsSettings != null) {
container_allow_insecure?.visibility = View.VISIBLE
val allowinsecure =
Utils.arrayFind(allowinsecures, tlsSetting.allowInsecure.toString())
if (allowinsecure >= 0) {
sp_allow_insecure?.setSelection(allowinsecure)
}
container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE
} else { // reality settings
container_public_key?.visibility = View.VISIBLE
et_public_key?.text = Utils.getEditable(tlsSetting.publicKey.orEmpty())
container_short_id?.visibility = View.VISIBLE
et_short_id?.text = Utils.getEditable(tlsSetting.shortId.orEmpty())
container_spider_x?.visibility = View.VISIBLE
et_spider_x?.text = Utils.getEditable(tlsSetting.spiderX.orEmpty())
container_allow_insecure?.visibility = View.GONE
}
}
if (streamSetting.tlsSettings == null && streamSetting.realitySettings == null) {
container_sni?.visibility = View.GONE
container_fingerprint?.visibility = View.GONE
container_alpn?.visibility = View.GONE
container_allow_insecure?.visibility = View.GONE
container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE
} }
} }
val network = Utils.arrayFind(networks, streamSetting.network) val network = Utils.arrayFind(networks, streamSetting.network)
@@ -383,13 +209,6 @@ class ServerActivity : BaseActivity() {
//et_security.text = null //et_security.text = null
sp_flow?.setSelection(0) sp_flow?.setSelection(0)
et_public_key?.text = null
et_reserved1?.text = Utils.getEditable("0")
et_reserved2?.text = Utils.getEditable("0")
et_reserved3?.text = Utils.getEditable("0")
et_local_address?.text =
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
return true return true
} }
@@ -410,20 +229,9 @@ class ServerActivity : BaseActivity() {
toast(R.string.server_lab_port) toast(R.string.server_lab_port)
return false return false
} }
val config = val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(createConfigType)
MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(createConfigType) if (config.configType != EConfigType.SOCKS && TextUtils.isEmpty(et_id.text.toString())) {
if (config.configType != EConfigType.SOCKS toast(R.string.server_lab_id)
&& config.configType != EConfigType.HTTP
&& TextUtils.isEmpty(et_id.text.toString())
) {
if (config.configType == EConfigType.TROJAN
|| config.configType == EConfigType.SHADOWSOCKS
|| config.configType == EConfigType.HYSTERIA2
) {
toast(R.string.server_lab_id3)
} else {
toast(R.string.server_lab_id)
}
return false return false
} }
sp_stream_security?.let { sp_stream_security?.let {
@@ -447,20 +255,11 @@ class ServerActivity : BaseActivity() {
config.outboundBean?.settings?.servers?.get(0)?.let { server -> config.outboundBean?.settings?.servers?.get(0)?.let { server ->
saveServers(server, port, config) saveServers(server, port, config)
} }
val wireguard = config.outboundBean?.settings
wireguard?.peers?.get(0)?.let { _ ->
savePeer(wireguard, port)
}
config.outboundBean?.streamSettings?.let { config.outboundBean?.streamSettings?.let {
val sni = saveStreamSettings(it) saveStreamSettings(it)
saveTls(it, sni)
} }
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) { if(config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
config.subscriptionId = subscriptionId.orEmpty() config.subscriptionId = subscriptionId!!
}
if (config.configType == EConfigType.HYSTERIA2) {
config.outboundBean?.settings?.obfsPassword = et_obfs_password?.text?.toString()
} }
MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerConfig(editGuid, config)
@@ -469,11 +268,7 @@ class ServerActivity : BaseActivity() {
return true return true
} }
private fun saveVnext( private fun saveVnext(vnext: V2rayConfig.OutboundBean.OutSettingsBean.VnextBean, port: Int, config: ServerConfig) {
vnext: V2rayConfig.OutboundBean.OutSettingsBean.VnextBean,
port: Int,
config: ServerConfig
) {
vnext.address = et_address.text.toString().trim() vnext.address = et_address.text.toString().trim()
vnext.port = port vnext.port = port
vnext.users[0].id = et_id.text.toString().trim() vnext.users[0].id = et_id.text.toString().trim()
@@ -482,119 +277,82 @@ class ServerActivity : BaseActivity() {
vnext.users[0].security = securitys[sp_security?.selectedItemPosition ?: 0] vnext.users[0].security = securitys[sp_security?.selectedItemPosition ?: 0]
} else if (config.configType == EConfigType.VLESS) { } else if (config.configType == EConfigType.VLESS) {
vnext.users[0].encryption = et_security?.text.toString().trim() vnext.users[0].encryption = et_security?.text.toString().trim()
vnext.users[0].flow = flows[sp_flow?.selectedItemPosition ?: 0] if (streamSecuritys[sp_stream_security?.selectedItemPosition ?: 0] == XTLS) {
vnext.users[0].flow = flows[sp_flow?.selectedItemPosition ?: 0].ifBlank { DEFAULT_FLOW }
} else {
vnext.users[0].flow = ""
}
} }
} }
private fun saveServers( private fun saveServers(server: V2rayConfig.OutboundBean.OutSettingsBean.ServersBean, port: Int, config: ServerConfig) {
server: V2rayConfig.OutboundBean.OutSettingsBean.ServersBean,
port: Int,
config: ServerConfig
) {
server.address = et_address.text.toString().trim() server.address = et_address.text.toString().trim()
server.port = port server.port = port
if (config.configType == EConfigType.SHADOWSOCKS) { if (config.configType == EConfigType.SHADOWSOCKS) {
server.password = et_id.text.toString().trim() server.password = et_id.text.toString().trim()
server.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0] server.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0]
} else if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) { } else if (config.configType == EConfigType.SOCKS) {
if (TextUtils.isEmpty(et_security?.text) && TextUtils.isEmpty(et_id.text)) { if (TextUtils.isEmpty(et_security?.text) && TextUtils.isEmpty(et_id.text)) {
server.users = null server.users = null
} else { } else {
val socksUsersBean = val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = et_security?.text.toString().trim() socksUsersBean.user = et_security?.text.toString().trim()
socksUsersBean.pass = et_id.text.toString().trim() socksUsersBean.pass = et_id.text.toString().trim()
server.users = listOf(socksUsersBean) server.users = listOf(socksUsersBean)
} }
} else if (config.configType == EConfigType.TROJAN || config.configType == EConfigType.HYSTERIA2) { } else if (config.configType == EConfigType.TROJAN) {
server.password = et_id.text.toString().trim() server.password = et_id.text.toString().trim()
server.flow =
if (streamSecuritys[sp_stream_security?.selectedItemPosition ?: 0] == XTLS) {
flows[sp_flow?.selectedItemPosition ?: 0].ifBlank { DEFAULT_FLOW }
} else {
""
}
} }
} }
private fun savePeer(wireguard: V2rayConfig.OutboundBean.OutSettingsBean, port: Int) { private fun saveStreamSettings(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean) {
wireguard.secretKey = et_id.text.toString().trim() val network = sp_network?.selectedItemPosition ?: return
wireguard.peers?.get(0)?.publicKey = et_public_key?.text.toString().trim() val type = sp_header_type?.selectedItemPosition ?: return
wireguard.peers?.get(0)?.endpoint = val requestHost = et_request_host?.text?.toString()?.trim() ?: return
getIpv6Address(et_address.text.toString().trim()) + ":" + port val path = et_path?.text?.toString()?.trim() ?: return
val reserved1 = Utils.parseInt(et_reserved1?.text.toString()) val sniField = et_sni?.text?.toString()?.trim() ?: return
val reserved2 = Utils.parseInt(et_reserved2?.text.toString()) val allowInsecureField = sp_allow_insecure?.selectedItemPosition ?: return
val reserved3 = Utils.parseInt(et_reserved3?.text.toString())
if (reserved1 > 0 || reserved2 > 0 || reserved3 > 0) {
wireguard.reserved = listOf(reserved1, reserved2, reserved3)
} else {
wireguard.reserved = null
}
wireguard.address = et_local_address?.text.toString().removeWhiteSpace().split(",")
wireguard.mtu = Utils.parseInt(et_local_mtu?.text.toString())
}
private fun saveStreamSettings(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean): String? {
val network = sp_network?.selectedItemPosition ?: return null
val type = sp_header_type?.selectedItemPosition ?: return null
val requestHost = et_request_host?.text?.toString()?.trim() ?: return null
val path = et_path?.text?.toString()?.trim() ?: return null
val sni = streamSetting.populateTransportSettings(
transport = networks[network],
headerType = transportTypes(networks[network])[type],
host = requestHost,
path = path,
seed = path,
quicSecurity = requestHost,
key = path,
mode = transportTypes(networks[network])[type],
serviceName = path,
authority = requestHost,
)
return sni
}
private fun saveTls(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean, sni: String?) {
val streamSecurity = sp_stream_security?.selectedItemPosition ?: return val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
val sniField = et_sni?.text?.toString()?.trim() var utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: return
val allowInsecureField = sp_allow_insecure?.selectedItemPosition
val utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: 0
val alpnIndex = sp_stream_alpn?.selectedItemPosition ?: 0
val publicKey = et_public_key?.text?.toString()
val shortId = et_short_id?.text?.toString()
val spiderX = et_spider_x?.text?.toString()
val allowInsecure = if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) { var sni = streamSetting.populateTransportSettings(
transport = networks[network],
headerType = transportTypes(networks[network])[type],
host = requestHost,
path = path,
seed = path,
quicSecurity = requestHost,
key = path,
mode = transportTypes(networks[network])[type],
serviceName = path
)
if (sniField.isNotBlank()) {
sni = sniField
}
val allowInsecure = if (allowinsecures[allowInsecureField].isBlank()) {
settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false
} else { } else {
allowinsecures[allowInsecureField].toBoolean() allowinsecures[allowInsecureField].toBoolean()
} }
streamSetting.populateTlsSettings( streamSetting.populateTlsSettings(streamSecuritys[streamSecurity], allowInsecure, sni, uTlsItems[utlsIndex])
streamSecurity = streamSecuritys[streamSecurity],
allowInsecure = allowInsecure,
sni = sniField ?: sni ?: "",
fingerprint = uTlsItems[utlsIndex],
alpns = alpns[alpnIndex],
publicKey = publicKey,
shortId = shortId,
spiderX = spiderX
)
} }
private fun transportTypes(network: String?): Array<out String> { private fun transportTypes(network: String?): Array<out String> {
return when (network) { return if (network == "tcp") {
"tcp" -> { tcpTypes
tcpTypes } else if (network == "kcp" || network == "quic") {
} kcpAndQuicTypes
} else if (network == "grpc") {
"kcp", "quic" -> { grpcModes
kcpAndQuicTypes } else {
} arrayOf("---")
"grpc" -> {
grpcModes
}
else -> {
arrayOf("---")
}
} }
} }
@@ -603,23 +361,18 @@ class ServerActivity : BaseActivity() {
*/ */
private fun deleteServer(): Boolean { private fun deleteServer(): Boolean {
if (editGuid.isNotEmpty()) { if (editGuid.isNotEmpty()) {
if (editGuid != MmkvManager.getSelectServer()) { if (editGuid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
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.removeServer(editGuid) MmkvManager.removeServer(editGuid)
finish() finish()
} }
.setNegativeButton(android.R.string.no) { _, _ ->
// do nothing
}
.show() .show()
} else { } else {
MmkvManager.removeServer(editGuid) MmkvManager.removeServer(editGuid)
finish() finish()
} }
} else {
application.toast(R.string.toast_action_not_allowed)
} }
} }
return true return true
@@ -647,12 +400,10 @@ class ServerActivity : 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)
} }
} }
@@ -6,38 +6,38 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast 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.language.json.JsonLanguage import com.blacksquircle.ui.language.json.JsonLanguage
import com.google.gson.*
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
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.JsonUtil
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 me.drakeet.support.toast.ToastCompat import me.drakeet.support.toast.ToastCompat
class ServerCustomConfigActivity : BaseActivity() { class ServerCustomConfigActivity : BaseActivity() {
private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) } private lateinit var binding: ActivityServerCustomConfigBinding
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() } private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() }
private val isRunning by lazy { private val isRunning by lazy {
intent.getBooleanExtra("isRunning", false) intent.getBooleanExtra("isRunning", false)
&& editGuid.isNotEmpty() && editGuid.isNotEmpty()
&& editGuid == MmkvManager.getSelectServer() && editGuid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) binding = ActivityServerCustomConfigBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
title = getString(R.string.title_server) title = getString(R.string.title_server)
if (!Utils.getDarkModeStatus(this)) {
binding.editor.colorScheme = EditorTheme.INTELLIJ_LIGHT
}
binding.editor.language = JsonLanguage() binding.editor.language = JsonLanguage()
val config = MmkvManager.decodeServerConfig(editGuid) val config = MmkvManager.decodeServerConfig(editGuid)
if (config != null) { if (config != null) {
@@ -45,6 +45,7 @@ class ServerCustomConfigActivity : BaseActivity() {
} else { } else {
clearServer() clearServer()
} }
supportActionBar?.setDisplayHomeAsUpEnabled(true)
} }
/** /**
@@ -52,7 +53,7 @@ class ServerCustomConfigActivity : BaseActivity() {
*/ */
private fun bindingServer(config: ServerConfig): Boolean { private fun bindingServer(config: ServerConfig): Boolean {
binding.etRemarks.text = Utils.getEditable(config.remarks) binding.etRemarks.text = Utils.getEditable(config.remarks)
val raw = MmkvManager.decodeServerRaw(editGuid) val raw = serverRawStorage?.decodeString(editGuid)
if (raw.isNullOrBlank()) { if (raw.isNullOrBlank()) {
binding.editor.setTextContent(Utils.getEditable(config.fullConfig?.toPrettyPrinting().orEmpty())) binding.editor.setTextContent(Utils.getEditable(config.fullConfig?.toPrettyPrinting().orEmpty()))
} else { } else {
@@ -79,7 +80,7 @@ class ServerCustomConfigActivity : BaseActivity() {
} }
val v2rayConfig = try { val v2rayConfig = try {
JsonUtil.fromJson(binding.editor.text.toString(), V2rayConfig::class.java) Gson().fromJson(binding.editor.text.toString(), V2rayConfig::class.java)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
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()
@@ -87,11 +88,11 @@ class ServerCustomConfigActivity : BaseActivity() {
} }
val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM) val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM)
config.remarks = if (binding.etRemarks.text.isNullOrEmpty()) v2rayConfig.remarks.orEmpty() else binding.etRemarks.text.toString() config.remarks = binding.etRemarks.text.toString().trim()
config.fullConfig = v2rayConfig config.fullConfig = v2rayConfig
MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerConfig(editGuid, config)
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString()) serverRawStorage?.encode(editGuid, binding.editor.text.toString())
toast(R.string.toast_success) toast(R.string.toast_success)
finish() finish()
return true return true
@@ -103,14 +104,11 @@ class ServerCustomConfigActivity : BaseActivity() {
private fun deleteServer(): Boolean { private fun deleteServer(): Boolean {
if (editGuid.isNotEmpty()) { if (editGuid.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) { _, _ ->
MmkvManager.removeServer(editGuid) MmkvManager.removeServer(editGuid)
finish() finish()
} }
.setNegativeButton(android.R.string.no) { _, _ -> .show()
// do nothing
}
.show()
} }
return true return true
} }
@@ -137,12 +135,10 @@ 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)
} }
} }
@@ -5,23 +5,11 @@ import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.view.View import android.view.View
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.preference.CheckBoxPreference import androidx.preference.*
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceFragmentCompat
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequest
import androidx.work.multiprocess.RemoteWorkManager
import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.VPN
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.extension.toLongEx
import com.v2ray.ang.service.SubscriptionUpdater
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.SettingsViewModel import com.v2ray.ang.viewmodel.SettingsViewModel
import java.util.concurrent.TimeUnit
class SettingsActivity : BaseActivity() { class SettingsActivity : BaseActivity() {
private val settingsViewModel: SettingsViewModel by viewModels() private val settingsViewModel: SettingsViewModel by viewModels()
@@ -32,110 +20,91 @@ class SettingsActivity : BaseActivity() {
title = getString(R.string.title_settings) title = getString(R.string.title_settings)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
settingsViewModel.startListenPreferenceChange() settingsViewModel.startListenPreferenceChange()
} }
class SettingsFragment : PreferenceFragmentCompat() { class SettingsFragment : PreferenceFragmentCompat() {
private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) } private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) }
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) } private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) } private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) } private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) } private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
// val autoRestart by lazy { findPreference(PREF_AUTO_RESTART) as CheckBoxPreference }
private val mux by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_MUX_ENABLED) }
private val muxConcurrency by lazy { findPreference<EditTextPreference>(AppConfig.PREF_MUX_CONCURRENCY) }
private val muxXudpConcurrency by lazy { findPreference<EditTextPreference>(AppConfig.PREF_MUX_XUDP_CONCURRENCY) }
private val muxXudpQuic by lazy { findPreference<ListPreference>(AppConfig.PREF_MUX_XUDP_QUIC) }
private val fragment by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FRAGMENT_ENABLED) }
private val fragmentPackets by lazy { findPreference<ListPreference>(AppConfig.PREF_FRAGMENT_PACKETS) }
private val fragmentLength by lazy { findPreference<EditTextPreference>(AppConfig.PREF_FRAGMENT_LENGTH) }
private val fragmentInterval by lazy { findPreference<EditTextPreference>(AppConfig.PREF_FRAGMENT_INTERVAL) }
private val autoUpdateCheck by lazy { findPreference<CheckBoxPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE) }
private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_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 socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
private val httpPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HTTP_PORT) }
private val routingCustom by lazy { findPreference<Preference>(AppConfig.PREF_ROUTING_CUSTOM) }
// val licenses: Preference by lazy { findPreference(PREF_LICENSES) }
// val feedback: Preference by lazy { findPreference(PREF_FEEDBACK) }
// val tgGroup: Preference by lazy { findPreference(PREF_TG_GROUP) }
private val mode by lazy { findPreference<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?) {
addPreferencesFromResource(R.xml.pref_settings) addPreferencesFromResource(R.xml.pref_settings)
routingCustom?.setOnPreferenceClickListener {
startActivity(Intent(activity, RoutingSettingsActivity::class.java))
false
}
// licenses.onClick {
// val fragment = LicensesDialogFragment.Builder(act)
// .setNotices(R.raw.licenses)
// .setIncludeOwnLicense(false)
// .build()
// fragment.show((act as AppCompatActivity).supportFragmentManager, null)
// }
//
// feedback.onClick {
// Utils.openUri(activity, "https://github.com/2dust/v2rayNG/issues")
// }
// tgGroup.onClick {
// // Utils.openUri(activity, "https://t.me/v2rayN")
// val intent = Intent(Intent.ACTION_VIEW, Uri.parse("tg:resolve?domain=v2rayN"))
// try {
// startActivity(intent)
// } catch (e: Exception) {
// e.printStackTrace()
// toast(R.string.toast_tg_app_not_found)
// }
// }
perAppProxy?.setOnPreferenceClickListener { perAppProxy?.setOnPreferenceClickListener {
startActivity(Intent(activity, PerAppProxyActivity::class.java)) startActivity(Intent(activity, PerAppProxyActivity::class.java))
perAppProxy?.isChecked = true perAppProxy?.isChecked = true
false false
} }
localDns?.setOnPreferenceChangeListener { _, any ->
remoteDns?.setOnPreferenceChangeListener { _, any ->
// remoteDns.summary = any as String
val nval = any as String
remoteDns?.summary = if (nval == "") AppConfig.DNS_AGENT else nval
true
}
domesticDns?.setOnPreferenceChangeListener { _, any ->
// domesticDns.summary = any as String
val nval = any as String
domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
true
}
localDns?.setOnPreferenceChangeListener{ _, any ->
updateLocalDns(any as Boolean) updateLocalDns(any as Boolean)
true true
} }
localDnsPort?.setOnPreferenceChangeListener { _, any -> localDnsPort?.setOnPreferenceChangeListener { _, any ->
val nval = any as String val nval = any as String
localDnsPort?.summary = localDnsPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval
if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval
true true
} }
vpnDns?.setOnPreferenceChangeListener { _, any -> vpnDns?.setOnPreferenceChangeListener { _, any ->
vpnDns?.summary = any as String vpnDns?.summary = any as String
true true
} }
mux?.setOnPreferenceChangeListener { _, newValue ->
updateMux(newValue as Boolean)
true
}
muxConcurrency?.setOnPreferenceChangeListener { _, newValue ->
updateMuxConcurrency(newValue as String)
true
}
muxXudpConcurrency?.setOnPreferenceChangeListener { _, newValue ->
updateMuxXudpConcurrency(newValue as String)
true
}
fragment?.setOnPreferenceChangeListener { _, newValue ->
updateFragment(newValue as Boolean)
true
}
fragmentPackets?.setOnPreferenceChangeListener { _, newValue ->
updateFragmentPackets(newValue as String)
true
}
fragmentLength?.setOnPreferenceChangeListener { _, newValue ->
updateFragmentLength(newValue as String)
true
}
fragmentInterval?.setOnPreferenceChangeListener { _, newValue ->
updateFragmentInterval(newValue as String)
true
}
autoUpdateCheck?.setOnPreferenceChangeListener { _, newValue ->
val value = newValue as Boolean
autoUpdateCheck?.isChecked = value
autoUpdateInterval?.isEnabled = value
autoUpdateInterval?.text?.toLongEx()?.let {
if (newValue) configureUpdateTask(it) else cancelUpdateTask()
}
true
}
autoUpdateInterval?.setOnPreferenceChangeListener { _, any ->
var nval = any as String
// It must be greater than 15 minutes because WorkManager couldn't run tasks under 15 minutes intervals
nval =
if (TextUtils.isEmpty(nval) || nval.toLongEx() < 15) AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL else nval
autoUpdateInterval?.summary = nval
configureUpdateTask(nval.toLongEx())
true
}
socksPort?.setOnPreferenceChangeListener { _, any -> 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
@@ -146,133 +115,57 @@ 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
} }
mode?.dialogLayoutResource = R.layout.preference_with_help_link mode?.dialogLayoutResource = R.layout.preference_with_help_link
//loglevel.summary = "LogLevel" //loglevel.summary = "LogLevel"
} }
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
updateMode(settingsStorage.decodeString(AppConfig.PREF_MODE, VPN)) val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
localDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false) updateMode(defaultSharedPreferences.getString(AppConfig.PREF_MODE, "VPN"))
fakeDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FAKE_DNS_ENABLED, false) var remoteDnsString = defaultSharedPreferences.getString(AppConfig.PREF_REMOTE_DNS, "")
localDnsPort?.summary = settingsStorage.decodeString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS) domesticDns?.summary = defaultSharedPreferences.getString(AppConfig.PREF_DOMESTIC_DNS, "")
vpnDns?.summary = settingsStorage.decodeString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
updateMux(settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false)) localDnsPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
mux?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false) socksPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
muxConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_CONCURRENCY, "8") httpPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
muxXudpConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
updateFragment(settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false)) if (TextUtils.isEmpty(remoteDnsString)) {
fragment?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false) remoteDnsString = AppConfig.DNS_AGENT
fragmentPackets?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")
fragmentLength?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
fragmentInterval?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
autoUpdateCheck?.isChecked = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
autoUpdateInterval?.summary =
settingsStorage.decodeString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
autoUpdateInterval?.isEnabled = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
socksPort?.summary = settingsStorage.decodeString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
httpPort?.summary = settingsStorage.decodeString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
remoteDns?.summary = settingsStorage.decodeString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
domesticDns?.summary = settingsStorage.decodeString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
delayTestUrl?.summary = settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl)
initSharedPreference()
}
private fun initSharedPreference() {
listOf(
localDnsPort,
vpnDns,
muxConcurrency,
muxXudpConcurrency,
fragmentLength,
fragmentInterval,
autoUpdateInterval,
socksPort,
httpPort,
remoteDns,
domesticDns,
delayTestUrl
).forEach { key ->
key?.text = key?.summary.toString()
} }
if (TextUtils.isEmpty(domesticDns?.summary)) {
listOf( domesticDns?.summary = AppConfig.DNS_DIRECT
AppConfig.PREF_SNIFFING_ENABLED,
).forEach { key ->
findPreference<CheckBoxPreference>(key)?.isChecked =
settingsStorage.decodeBool(key, true)
} }
remoteDns?.summary = remoteDnsString
vpnDns?.summary = defaultSharedPreferences.getString(AppConfig.PREF_VPN_DNS, remoteDnsString)
listOf( if (TextUtils.isEmpty(localDnsPort?.summary)) {
AppConfig.PREF_ROUTE_ONLY_ENABLED, localDnsPort?.summary = AppConfig.PORT_LOCAL_DNS
AppConfig.PREF_IS_BOOTED,
AppConfig.PREF_BYPASS_APPS,
AppConfig.PREF_SPEED_ENABLED,
AppConfig.PREF_CONFIRM_REMOVE,
AppConfig.PREF_START_SCAN_IMMEDIATE,
AppConfig.PREF_PREFER_IPV6,
AppConfig.PREF_PROXY_SHARING,
AppConfig.PREF_ALLOW_INSECURE
).forEach { key ->
findPreference<CheckBoxPreference>(key)?.isChecked =
settingsStorage.decodeBool(key, false)
} }
if (TextUtils.isEmpty(socksPort?.summary)) {
listOf( socksPort?.summary = AppConfig.PORT_SOCKS
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, }
AppConfig.PREF_MUX_XUDP_QUIC, if (TextUtils.isEmpty(httpPort?.summary)) {
AppConfig.PREF_FRAGMENT_PACKETS, httpPort?.summary = AppConfig.PORT_HTTP
AppConfig.PREF_LANGUAGE,
AppConfig.PREF_UI_MODE_NIGHT,
AppConfig.PREF_LOGLEVEL,
AppConfig.PREF_MODE
).forEach { key ->
if (settingsStorage.decodeString(key) != null) {
findPreference<ListPreference>(key)?.value = settingsStorage.decodeString(key)
}
} }
} }
private fun updateMode(mode: String?) { private fun updateMode(mode: String?) {
val vpn = mode == VPN val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
val vpn = mode == "VPN"
perAppProxy?.isEnabled = vpn perAppProxy?.isEnabled = vpn
perAppProxy?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false) perAppProxy?.isChecked = PreferenceManager.getDefaultSharedPreferences(requireActivity())
.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
localDns?.isEnabled = vpn localDns?.isEnabled = vpn
fakeDns?.isEnabled = vpn fakeDns?.isEnabled = vpn
localDnsPort?.isEnabled = vpn localDnsPort?.isEnabled = vpn
vpnDns?.isEnabled = vpn vpnDns?.isEnabled = vpn
if (vpn) { if (vpn) {
updateLocalDns( updateLocalDns(defaultSharedPreferences.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false))
settingsStorage.getBoolean(
AppConfig.PREF_LOCAL_DNS_ENABLED,
false
)
)
} }
} }
@@ -281,80 +174,6 @@ class SettingsActivity : BaseActivity() {
localDnsPort?.isEnabled = enabled localDnsPort?.isEnabled = enabled
vpnDns?.isEnabled = !enabled vpnDns?.isEnabled = !enabled
} }
private fun configureUpdateTask(interval: Long) {
val rw = RemoteWorkManager.getInstance(AngApplication.application)
rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME)
rw.enqueueUniquePeriodicWork(
AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME,
ExistingPeriodicWorkPolicy.UPDATE,
PeriodicWorkRequest.Builder(
SubscriptionUpdater.UpdateTask::class.java,
interval,
TimeUnit.MINUTES
)
.apply {
setInitialDelay(interval, TimeUnit.MINUTES)
}
.build()
)
}
private fun cancelUpdateTask() {
val rw = RemoteWorkManager.getInstance(AngApplication.application)
rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME)
}
private fun updateMux(enabled: Boolean) {
muxConcurrency?.isEnabled = enabled
muxXudpConcurrency?.isEnabled = enabled
muxXudpQuic?.isEnabled = enabled
if (enabled) {
updateMuxConcurrency(settingsStorage.decodeString(AppConfig.PREF_MUX_CONCURRENCY, "8"))
updateMuxXudpConcurrency(settingsStorage.decodeString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8"))
}
}
private fun updateMuxConcurrency(value: String?) {
if (value == null) {
} else {
val concurrency = value.toIntOrNull() ?: 8
muxConcurrency?.summary = concurrency.toString()
}
}
private fun updateMuxXudpConcurrency(value: String?) {
if (value == null) {
muxXudpQuic?.isEnabled = true
} else {
val concurrency = value.toIntOrNull() ?: 8
muxXudpConcurrency?.summary = concurrency.toString()
muxXudpQuic?.isEnabled = concurrency >= 0
}
}
private fun updateFragment(enabled: Boolean) {
fragmentPackets?.isEnabled = enabled
fragmentLength?.isEnabled = enabled
fragmentInterval?.isEnabled = enabled
if (enabled) {
updateFragmentPackets(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello"))
updateFragmentLength(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100"))
updateFragmentInterval(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
}
}
private fun updateFragmentPackets(value: String?) {
fragmentPackets?.summary = value.toString()
}
private fun updateFragmentLength(value: String?) {
fragmentLength?.summary = value.toString()
}
private fun updateFragmentInterval(value: String?) {
fragmentInterval?.summary = value.toString()
}
} }
fun onModeHelpClicked(view: View) { fun onModeHelpClicked(view: View) {
@@ -5,35 +5,38 @@ 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.lifecycle.lifecycleScope import com.google.gson.Gson
import com.tencent.mmkv.MMKV
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.util.MmkvManager import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class SubEditActivity : BaseActivity() { class SubEditActivity : BaseActivity() {
private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) } private lateinit var binding: ActivitySubEditBinding
var del_config: MenuItem? = null var del_config: MenuItem? = null
var save_config: MenuItem? = null var save_config: MenuItem? = null
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() } private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) binding = ActivitySubEditBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
title = getString(R.string.title_sub_setting) title = getString(R.string.title_sub_setting)
val subItem = MmkvManager.decodeSubscription(editSubId) val json = subStorage?.decodeString(editSubId)
if (subItem != null) { if (!json.isNullOrBlank()) {
bindingServer(subItem) bindingServer(Gson().fromJson(json, SubscriptionItem::class.java))
} else { } else {
clearServer() clearServer()
} }
supportActionBar?.setDisplayHomeAsUpEnabled(true)
} }
/** /**
@@ -42,11 +45,7 @@ class SubEditActivity : BaseActivity() {
private fun bindingServer(subItem: SubscriptionItem): Boolean { private fun bindingServer(subItem: SubscriptionItem): Boolean {
binding.etRemarks.text = Utils.getEditable(subItem.remarks) binding.etRemarks.text = Utils.getEditable(subItem.remarks)
binding.etUrl.text = Utils.getEditable(subItem.url) binding.etUrl.text = Utils.getEditable(subItem.url)
binding.etFilter.text = Utils.getEditable(subItem.filter)
binding.chkEnable.isChecked = subItem.enabled binding.chkEnable.isChecked = subItem.enabled
binding.autoUpdateCheck.isChecked = subItem.autoUpdate
binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile)
binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile)
return true return true
} }
@@ -56,10 +55,7 @@ class SubEditActivity : BaseActivity() {
private fun clearServer(): Boolean { private fun clearServer(): Boolean {
binding.etRemarks.text = null binding.etRemarks.text = null
binding.etUrl.text = null binding.etUrl.text = null
binding.etFilter.text = null
binding.chkEnable.isChecked = true binding.chkEnable.isChecked = true
binding.etPreProfile.text = null
binding.etNextProfile.text = null
return true return true
} }
@@ -67,15 +63,19 @@ class SubEditActivity : BaseActivity() {
* save server config * save server config
*/ */
private fun saveServer(): Boolean { private fun saveServer(): Boolean {
val subItem = MmkvManager.decodeSubscription(editSubId) ?: SubscriptionItem() val subItem: SubscriptionItem
val json = subStorage?.decodeString(editSubId)
var subId = editSubId
if (!json.isNullOrBlank()) {
subItem = Gson().fromJson(json, SubscriptionItem::class.java)
} else {
subId = Utils.getUuid()
subItem = SubscriptionItem()
}
subItem.remarks = binding.etRemarks.text.toString() subItem.remarks = binding.etRemarks.text.toString()
subItem.url = binding.etUrl.text.toString() subItem.url = binding.etUrl.text.toString()
subItem.filter = binding.etFilter.text.toString()
subItem.enabled = binding.chkEnable.isChecked subItem.enabled = binding.chkEnable.isChecked
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
subItem.prevProfile = binding.etPreProfile.text.toString()
subItem.nextProfile = binding.etNextProfile.text.toString()
if (TextUtils.isEmpty(subItem.remarks)) { if (TextUtils.isEmpty(subItem.remarks)) {
toast(R.string.sub_setting_remarks) toast(R.string.sub_setting_remarks)
@@ -86,7 +86,7 @@ class SubEditActivity : BaseActivity() {
// return false // return false
// } // }
MmkvManager.encodeSubscription(editSubId, subItem) subStorage?.encode(subId, Gson().toJson(subItem))
toast(R.string.toast_success) toast(R.string.toast_success)
finish() finish()
return true return true
@@ -98,18 +98,11 @@ class SubEditActivity : BaseActivity() {
private fun deleteServer(): Boolean { private fun deleteServer(): Boolean {
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()
}
} }
} .show()
.setNegativeButton(android.R.string.no) { _, _ ->
// do nothing
}
.show()
} }
return true return true
} }
@@ -131,13 +124,10 @@ 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)
} }
} }
@@ -1,35 +1,26 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.content.Intent import android.content.Intent
import android.os.Bundle import androidx.recyclerview.widget.LinearLayoutManager
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.ItemTouchHelper
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.helper.SimpleItemTouchHelperCallback
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 val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) } private lateinit var binding: ActivitySubSettingBinding
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) }
private var mItemTouchHelper: ItemTouchHelper? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) binding = ActivitySubSettingBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
title = getString(R.string.title_sub_setting) title = getString(R.string.title_sub_setting)
@@ -37,17 +28,20 @@ class SubSettingActivity : BaseActivity() {
binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) supportActionBar?.setDisplayHomeAsUpEnabled(true)
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
refreshData() subscriptions = MmkvManager.decodeSubscriptions()
adapter.notifyDataSetChanged()
} }
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)
} }
@@ -56,35 +50,6 @@ 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)
}
fun refreshData() {
subscriptions = MmkvManager.decodeSubscriptions()
adapter.notifyDataSetChanged()
} }
} }
@@ -2,30 +2,19 @@ package com.v2ray.ang.ui
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.view.ViewGroup
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ItemQrcodeBinding
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.QRCodeDecoder
import com.v2ray.ang.util.SettingsManager
import com.v2ray.ang.util.Utils
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter { class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>() {
private var mActivity: SubSettingActivity = activity private var mActivity: SubSettingActivity = activity
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
private val share_method: Array<out String> by lazy {
mActivity.resources.getStringArray(R.array.share_sub_method)
}
override fun getItemCount() = mActivity.subscriptions.size override fun getItemCount() = mActivity.subscriptions.size
@@ -34,90 +23,28 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
val subItem = mActivity.subscriptions[position].second val subItem = mActivity.subscriptions[position].second
holder.itemSubSettingBinding.tvName.text = subItem.remarks holder.itemSubSettingBinding.tvName.text = subItem.remarks
holder.itemSubSettingBinding.tvUrl.text = subItem.url holder.itemSubSettingBinding.tvUrl.text = subItem.url
holder.itemSubSettingBinding.chkEnable.isChecked = subItem.enabled if (subItem.enabled) {
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorSelected)
} else {
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorUnselected)
}
holder.itemView.setBackgroundColor(Color.TRANSPARENT) holder.itemView.setBackgroundColor(Color.TRANSPARENT)
holder.itemSubSettingBinding.layoutEdit.setOnClickListener { holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
mActivity.startActivity( mActivity.startActivity(Intent(mActivity, SubEditActivity::class.java)
Intent(mActivity, SubEditActivity::class.java)
.putExtra("subId", subId) .putExtra("subId", subId)
) )
} }
holder.itemSubSettingBinding.infoContainer.setOnClickListener {
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked -> subItem.enabled = !subItem.enabled
if( !it.isPressed) return@setOnCheckedChangeListener subStorage?.encode(subId, Gson().toJson(subItem))
subItem.enabled = isChecked notifyItemChanged(position)
MmkvManager.encodeSubscription(subId, subItem)
}
if (TextUtils.isEmpty(subItem.url)) {
holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
} else {
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
AlertDialog.Builder(mActivity)
.setItems(share_method.asList().toTypedArray()) { _, i ->
try {
when (i) {
0 -> {
val ivBinding =
ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
ivBinding.ivQcode.setImageBitmap(
QRCodeDecoder.createQRCode(
subItem.url
)
)
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
}
1 -> {
Utils.setClipboard(mActivity, subItem.url)
}
else -> mActivity.toast("else")
}
} catch (e: Exception) {
e.printStackTrace()
}
}.show()
}
} }
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
return MainViewHolder( return MainViewHolder(ItemRecyclerSubSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false))
ItemRecyclerSubSettingBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
} }
class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) : class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) : RecyclerView.ViewHolder(itemSubSettingBinding.root)
BaseViewHolder(itemSubSettingBinding.root), ItemTouchHelperViewHolder
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun onItemSelected() {
itemView.setBackgroundColor(Color.LTGRAY)
}
fun onItemClear() {
itemView.setBackgroundColor(0)
}
}
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
SettingsManager.swapSubscriptions(fromPosition, toPosition)
notifyItemMoved(fromPosition, toPosition)
return true
}
override fun onItemMoveCompleted() {
mActivity.refreshData()
}
override fun onItemDismiss(position: Int) {
}
} }
@@ -1,45 +1,51 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.content.Intent import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ListView import android.widget.ListView
import com.v2ray.ang.AppConfig import java.util.ArrayList
import com.v2ray.ang.R import com.v2ray.ang.R
import android.content.Intent
import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
import com.google.zxing.WriterException
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.databinding.ActivityTaskerBinding import com.v2ray.ang.databinding.ActivityTaskerBinding
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager
class TaskerActivity : BaseActivity() { class TaskerActivity : BaseActivity() {
private val binding by lazy { ActivityTaskerBinding.inflate(layoutInflater) } private lateinit var binding: ActivityTaskerBinding
private var listview: ListView? = null private var listview: ListView? = null
private var lstData: ArrayList<String> = ArrayList() private var lstData: ArrayList<String> = ArrayList()
private var lstGuid: ArrayList<String> = ArrayList() private var lstGuid: ArrayList<String> = ArrayList()
private val serverStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) binding = ActivityTaskerBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
//add def value //add def value
lstData.add("Default") lstData.add("Default")
lstGuid.add(AppConfig.TASKER_DEFAULT_GUID) lstGuid.add(AppConfig.TASKER_DEFAULT_GUID)
MmkvManager.decodeServerList()?.forEach { key -> serverStorage?.allKeys()?.forEach { key ->
MmkvManager.decodeServerConfig(key)?.let { config -> MmkvManager.decodeServerConfig(key)?.let { config ->
lstData.add(config.remarks) lstData.add(config.remarks)
lstGuid.add(key) lstGuid.add(key)
} }
} }
val adapter = ArrayAdapter( val adapter = ArrayAdapter(this,
this, android.R.layout.simple_list_item_single_choice, lstData)
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()
} }
@@ -59,7 +65,7 @@ class TaskerActivity : BaseActivity() {
listview?.setItemChecked(pos, true) listview?.setItemChecked(pos, true)
} }
} }
} catch (e: Exception) { } catch (e: WriterException) {
e.printStackTrace() e.printStackTrace()
} }
@@ -85,7 +91,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(RESULT_OK, intent) setResult(Activity.RESULT_OK, intent)
finish() finish()
} }
@@ -100,12 +106,10 @@ 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)
} }
@@ -3,75 +3,49 @@ 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 android.util.Log import com.google.zxing.WriterException
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
import com.v2ray.ang.util.AngConfigManager import com.v2ray.ang.util.AngConfigManager
import java.net.URLDecoder
class UrlSchemeActivity : BaseActivity() { class UrlSchemeActivity : BaseActivity() {
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) } private lateinit var binding: ActivityLogcatBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) binding = ActivityLogcatBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
var shareUrl: String = ""
try { try {
intent.apply { intent?.apply {
if (action == Intent.ACTION_SEND) { when (action) {
if ("text/plain" == type) { Intent.ACTION_SEND -> {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { if ("text/plain" == type) {
parseUri(it, null) intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
shareUrl = it
}
} }
} }
} else if (action == Intent.ACTION_VIEW) { Intent.ACTION_VIEW -> {
when (data?.host) { val uri: Uri? = intent.data
"install-config" -> { shareUrl = uri?.getQueryParameter("url")!!
val uri: Uri? = intent.data
val shareUrl = uri?.getQueryParameter("url").orEmpty()
parseUri(shareUrl, uri?.fragment)
}
"install-sub" -> {
val uri: Uri? = intent.data
val shareUrl = uri?.getQueryParameter("url").orEmpty()
parseUri(shareUrl, uri?.fragment)
}
else -> {
toast(R.string.toast_failure)
}
} }
} }
} }
toast(shareUrl)
val count = AngConfigManager.importBatchConfig(shareUrl, "", false)
if (count > 0) {
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
startActivity(Intent(this, MainActivity::class.java)) startActivity(Intent(this, MainActivity::class.java))
finish() finish()
} catch (e: Exception) { } catch (e: WriterException) {
e.printStackTrace() e.printStackTrace()
} }
} }
private fun parseUri(uriString: String?, fragment: String?) {
if (uriString.isNullOrEmpty()) {
return
}
Log.d("UrlScheme", uriString)
var decodedUrl = URLDecoder.decode(uriString, "UTF-8")
val uri = Uri.parse(decodedUrl)
if (uri != null) {
if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) {
decodedUrl += "#${fragment}"
}
Log.d("UrlScheme-decodedUrl", decodedUrl)
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
if (count + countSub > 0) {
toast(R.string.import_subscription_success)
} else {
toast(R.string.import_subscription_failure)
}
}
}
} }
@@ -2,36 +2,27 @@ 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.Bundle import android.os.Bundle
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.*
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.tbruyelle.rxpermissions3.RxPermissions import com.tbruyelle.rxpermissions.RxPermissions
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
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.extension.toTrafficString import com.v2ray.ang.extension.toTrafficString
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.SettingsManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -42,30 +33,28 @@ 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.Date import java.util.*
class UserAssetActivity : BaseActivity() { class UserAssetActivity : BaseActivity() {
private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) } private lateinit var binding: ActivitySubSettingBinding
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
val extDir by lazy { File(Utils.userAssetPath(this)) } val extDir by lazy { File(Utils.userAssetPath(this)) }
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat") val geofiles = arrayOf("geosite.dat", "geoip.dat")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) binding = ActivitySubSettingBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
title = getString(R.string.title_user_asset_setting) title = getString(R.string.title_user_asset_setting)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = UserAssetAdapter() binding.recyclerView.adapter = UserAssetAdapter()
} }
override fun onResume() {
super.onResume()
binding.recyclerView.adapter?.notifyDataSetChanged()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_asset, menu) menuInflater.inflate(R.menu.menu_asset, menu)
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
@@ -77,12 +66,6 @@ class UserAssetActivity : BaseActivity() {
true true
} }
R.id.add_url -> {
val intent = Intent(this, UserAssetUrlActivity::class.java)
startActivity(intent)
true
}
R.id.download_file -> { R.id.download_file -> {
downloadGeoFiles() downloadGeoFiles()
true true
@@ -92,56 +75,34 @@ class UserAssetActivity : BaseActivity() {
} }
private fun showFileChooser() { private fun showFileChooser() {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { RxPermissions(this).request(Manifest.permission.READ_EXTERNAL_STORAGE).subscribe {
Manifest.permission.READ_MEDIA_IMAGES if (it) {
} else { val intent = Intent(Intent.ACTION_GET_CONTENT)
Manifest.permission.READ_EXTERNAL_STORAGE intent.type = "*/*"
} intent.addCategory(Intent.CATEGORY_OPENABLE)
RxPermissions(this)
.request(permission)
.subscribe {
if (it) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
try { try {
chooseFile.launch( chooseFile.launch(
Intent.createChooser( Intent.createChooser(
intent, intent,
getString(R.string.title_file_chooser) getString(R.string.title_file_chooser)
)
) )
} catch (ex: android.content.ActivityNotFoundException) { )
toast(R.string.toast_require_file_manager) } catch (ex: android.content.ActivityNotFoundException) {
} toast(R.string.toast_require_file_manager)
} else }
toast(R.string.toast_permission_denied)
} }
}
} }
private val chooseFile = private val chooseFile =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { it -> registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) { if (it.resultCode == RESULT_OK && uri != null) {
val assetId = Utils.getUuid()
try { try {
val assetItem = AssetUrlItem(
getCursorName(uri) ?: uri.toString(),
"file"
)
// check remarks unique
val assetList = MmkvManager.decodeAssetUrls()
if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
toast(R.string.msg_remark_is_duplicate)
return@registerForActivityResult
}
MmkvManager.encodeAsset(assetId, assetItem)
copyFile(uri) copyFile(uri)
} catch (e: Exception) { } catch (e: Exception) {
toast(R.string.toast_asset_copy_failed) toast(R.string.toast_asset_copy_failed)
MmkvManager.removeAssetUrl(assetId)
} }
} }
} }
@@ -171,53 +132,39 @@ class UserAssetActivity : BaseActivity() {
} }
private fun downloadGeoFiles() { private fun downloadGeoFiles() {
val dialog = AlertDialog.Builder(this) val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
.setCancelable(false)
.show()
toast(R.string.msg_downloading_content) toast(R.string.msg_downloading_content)
geofiles.forEach {
val httpPort = SettingsManager.getHttpPort()
var assets = MmkvManager.decodeAssetUrls()
assets = addBuiltInGeoItems(assets)
assets.forEach {
//toast(getString(R.string.msg_downloading_content) + it) //toast(getString(R.string.msg_downloading_content) + it)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
var result = downloadGeo(it.second, 60000, httpPort) val result = downloadGeo(it, 60000, httpPort)
if (!result) {
result = downloadGeo(it.second, 60000, 0)
}
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
if (result) { if (result) {
toast(getString(R.string.toast_success) + " " + it.second.remarks) toast(getString(R.string.toast_success) + " " + it)
binding.recyclerView.adapter?.notifyDataSetChanged() binding.recyclerView.adapter?.notifyDataSetChanged()
} else { } else {
toast(getString(R.string.toast_failure) + " " + it.second.remarks) toast(getString(R.string.toast_failure) + " " + it)
} }
dialog.dismiss()
} }
} }
} }
} }
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean { private fun downloadGeo(name: String, timeout: Int, httpPort: Int): Boolean {
val targetTemp = File(extDir, item.remarks + "_temp") val url = AppConfig.geoUrl + name
val target = File(extDir, item.remarks) val targetTemp = File(extDir, name + "_temp")
val target = File(extDir, name)
var conn: HttpURLConnection? = null var conn: HttpURLConnection? = null
//Log.d(AppConfig.ANG_PACKAGE, url) //Log.d(AppConfig.ANG_PACKAGE, url)
try { try {
conn = if (httpPort == 0) { conn = URL(url).openConnection(
URL(item.url).openConnection() as HttpURLConnection Proxy(
} else { Proxy.Type.HTTP,
URL(item.url).openConnection( InetSocketAddress("127.0.0.1", httpPort)
Proxy( )
Proxy.Type.HTTP, ) as HttpURLConnection
InetSocketAddress(LOOPBACK, httpPort)
)
) as HttpURLConnection
}
conn.connectTimeout = timeout conn.connectTimeout = timeout
conn.readTimeout = timeout conn.readTimeout = timeout
val inputStream = conn.inputStream val inputStream = conn.inputStream
@@ -238,78 +185,32 @@ class UserAssetActivity : BaseActivity() {
} }
} }
private fun addBuiltInGeoItems(assets: List<Pair<String, AssetUrlItem>>): List<Pair<String, AssetUrlItem>> {
val list = mutableListOf<Pair<String, AssetUrlItem>>()
builtInGeoFiles
.filter { geoFile -> assets.none { it.second.remarks == geoFile } }
.forEach {
list.add(
Utils.getUuid() to AssetUrlItem(
it,
AppConfig.GeoUrl + it
)
)
}
return list + assets
}
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() { inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
return UserAssetViewHolder( return UserAssetViewHolder(ItemRecyclerUserAssetBinding.inflate(LayoutInflater.from(parent.context), parent, false))
ItemRecyclerUserAssetBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) { override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
var assets = MmkvManager.decodeAssetUrls() val file = extDir.listFiles()?.getOrNull(position) ?: return
assets = addBuiltInGeoItems(assets) holder.itemUserAssetBinding.assetName.text = file.name
val item = assets.getOrNull(position) ?: return val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
// file with name == item.second.remarks holder.itemUserAssetBinding.assetProperties.text = "${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}"
val file = extDir.listFiles()?.find { it.name == item.second.remarks } if (file.name in geofiles) {
holder.itemUserAssetBinding.assetName.text = item.second.remarks
if (file != null) {
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
holder.itemUserAssetBinding.assetProperties.text =
"${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}"
} else {
holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found)
}
if (item.second.remarks in builtInGeoFiles && item.second.url == AppConfig.GeoUrl + item.second.remarks) {
holder.itemUserAssetBinding.layoutEdit.visibility = GONE
holder.itemUserAssetBinding.layoutRemove.visibility = GONE holder.itemUserAssetBinding.layoutRemove.visibility = GONE
} else { } else {
holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE }
holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
} }
holder.itemUserAssetBinding.layoutEdit.setOnClickListener {
val intent = Intent(this@UserAssetActivity, UserAssetUrlActivity::class.java)
intent.putExtra("assetId", item.first)
startActivity(intent)
}
holder.itemUserAssetBinding.layoutRemove.setOnClickListener { holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
file?.delete() file.delete()
MmkvManager.removeAssetUrl(item.first)
binding.recyclerView.adapter?.notifyItemRemoved(position) binding.recyclerView.adapter?.notifyItemRemoved(position)
} }
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
var assets = MmkvManager.decodeAssetUrls() return extDir.listFiles()?.size ?: 0
assets = addBuiltInGeoItems(assets)
return assets.size
} }
} }
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) : class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) : RecyclerView.ViewHolder(itemUserAssetBinding.root)
RecyclerView.ViewHolder(itemUserAssetBinding.root)
} }
@@ -1,142 +0,0 @@
package com.v2ray.ang.ui
import android.os.Bundle
import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import java.io.File
class UserAssetUrlActivity : BaseActivity() {
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
var del_config: MenuItem? = null
var save_config: MenuItem? = null
val extDir by lazy { File(Utils.userAssetPath(this)) }
private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
title = getString(R.string.title_user_asset_add_url)
val assetItem = MmkvManager.decodeAsset(editAssetId)
if (assetItem != null) {
bindingAsset(assetItem)
} else {
clearAsset()
}
}
/**
* bingding seleced asset config
*/
private fun bindingAsset(assetItem: AssetUrlItem): Boolean {
binding.etRemarks.text = Utils.getEditable(assetItem.remarks)
binding.etUrl.text = Utils.getEditable(assetItem.url)
return true
}
/**
* clear or init asset config
*/
private fun clearAsset(): Boolean {
binding.etRemarks.text = null
binding.etUrl.text = null
return true
}
/**
* save asset config
*/
private fun saveServer(): Boolean {
var assetItem = MmkvManager.decodeAsset(editAssetId)
var assetId = editAssetId
if (assetItem != null) {
// remove file associated with the asset
val file = extDir.resolve(assetItem.remarks)
if (file.exists()) {
file.delete()
}
} else {
assetId = Utils.getUuid()
assetItem = AssetUrlItem()
}
assetItem.remarks = binding.etRemarks.text.toString()
assetItem.url = binding.etUrl.text.toString()
// check remarks unique
val assetList = MmkvManager.decodeAssetUrls()
if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
toast(R.string.msg_remark_is_duplicate)
return false
}
if (TextUtils.isEmpty(assetItem.remarks)) {
toast(R.string.sub_setting_remarks)
return false
}
if (TextUtils.isEmpty(assetItem.url)) {
toast(R.string.title_url)
return false
}
MmkvManager.encodeAsset(assetId, assetItem)
toast(R.string.toast_success)
finish()
return true
}
/**
* save server config
*/
private fun deleteServer(): Boolean {
if (editAssetId.isNotEmpty()) {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
MmkvManager.removeAssetUrl(editAssetId)
finish()
}
.setNegativeButton(android.R.string.no) { _, _ ->
// do nothing
}
.show()
}
return true
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.action_server, menu)
del_config = menu.findItem(R.id.del_config)
save_config = menu.findItem(R.id.save_config)
if (editAssetId.isEmpty()) {
del_config?.isVisible = false
}
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.del_config -> {
deleteServer()
true
}
R.id.save_config -> {
saveServer()
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -1,84 +1,355 @@
package com.v2ray.ang.util package com.v2ray.ang.util
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.graphics.Bitmap import android.graphics.Bitmap
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import androidx.preference.PreferenceManager
import com.google.gson.GsonBuilder import com.google.gson.Gson
import com.google.gson.JsonPrimitive import com.tencent.mmkv.MMKV
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.reflect.TypeToken
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.HY2 import com.v2ray.ang.AppConfig.ANG_CONFIG
import com.v2ray.ang.AppConfig.HTTPS_PROTOCOL
import com.v2ray.ang.AppConfig.HTTP_PROTOCOL
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.dto.* import com.v2ray.ang.dto.*
import com.v2ray.ang.util.fmt.Hysteria2Fmt import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_SECURITY
import com.v2ray.ang.util.fmt.ShadowsocksFmt import com.v2ray.ang.dto.V2rayConfig.Companion.TLS
import com.v2ray.ang.util.fmt.SocksFmt import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
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.net.URI import java.net.URI
import java.util.* import java.util.*
import com.v2ray.ang.extension.idnHost
object AngConfigManager { object AngConfigManager {
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
/** /**
* parse config form qrcode or... * Legacy loading config
*/ */
private fun parseConfig( fun migrateLegacyConfig(c: Context): Boolean? {
str: String?, try {
subid: String, val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(c)
subItem: SubscriptionItem?, val context = defaultSharedPreferences.getString(ANG_CONFIG, "")
removedSelectedServer: ServerConfig? if (context.isNullOrBlank()) {
): Int { return null
}
val angConfig = Gson().fromJson(context, AngConfig::class.java)
for (i in angConfig.vmess.indices) {
upgradeServerVersion(angConfig.vmess[i])
}
copyLegacySettings(defaultSharedPreferences)
migrateVmessBean(angConfig, defaultSharedPreferences)
migrateSubItemBean(angConfig)
defaultSharedPreferences.edit().remove(ANG_CONFIG).apply()
return true
} catch (e: Exception) {
e.printStackTrace()
}
return false
}
private fun copyLegacySettings(sharedPreferences: SharedPreferences) {
listOf(
AppConfig.PREF_MODE,
AppConfig.PREF_REMOTE_DNS,
AppConfig.PREF_DOMESTIC_DNS,
AppConfig.PREF_LOCAL_DNS_PORT,
AppConfig.PREF_SOCKS_PORT,
AppConfig.PREF_HTTP_PORT,
AppConfig.PREF_LOGLEVEL,
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
AppConfig.PREF_ROUTING_MODE,
AppConfig.PREF_V2RAY_ROUTING_AGENT,
AppConfig.PREF_V2RAY_ROUTING_BLOCKED,
AppConfig.PREF_V2RAY_ROUTING_DIRECT,
).forEach { key ->
settingsStorage?.encode(key, sharedPreferences.getString(key, null))
}
listOf(
AppConfig.PREF_SPEED_ENABLED,
AppConfig.PREF_PROXY_SHARING,
AppConfig.PREF_LOCAL_DNS_ENABLED,
AppConfig.PREF_ALLOW_INSECURE,
AppConfig.PREF_PREFER_IPV6,
AppConfig.PREF_PER_APP_PROXY,
AppConfig.PREF_BYPASS_APPS,
).forEach { key ->
settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false))
}
settingsStorage?.encode(AppConfig.PREF_SNIFFING_ENABLED, sharedPreferences.getBoolean(AppConfig.PREF_SNIFFING_ENABLED, true))
settingsStorage?.encode(AppConfig.PREF_PER_APP_PROXY_SET, sharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, setOf()))
}
private fun migrateVmessBean(angConfig: AngConfig, sharedPreferences: SharedPreferences) {
angConfig.vmess.forEachIndexed { index, vmessBean ->
val type = EConfigType.fromInt(vmessBean.configType) ?: return@forEachIndexed
val config = ServerConfig.create(type)
config.remarks = vmessBean.remarks
config.subscriptionId = vmessBean.subid
if (type == EConfigType.CUSTOM) {
val jsonConfig = sharedPreferences.getString(ANG_CONFIG + vmessBean.guid, "")
val v2rayConfig = try {
Gson().fromJson(jsonConfig, V2rayConfig::class.java)
} catch (e: Exception) {
e.printStackTrace()
return@forEachIndexed
}
config.fullConfig = v2rayConfig
serverRawStorage?.encode(vmessBean.guid, jsonConfig)
} else {
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = vmessBean.address
vnext.port = vmessBean.port
vnext.users[0].id = vmessBean.id
if (config.configType == EConfigType.VMESS) {
vnext.users[0].alterId = vmessBean.alterId
vnext.users[0].security = vmessBean.security
} else if (config.configType == EConfigType.VLESS) {
vnext.users[0].encryption = vmessBean.security
vnext.users[0].flow = vmessBean.flow
}
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = vmessBean.address
server.port = vmessBean.port
if (config.configType == EConfigType.SHADOWSOCKS) {
server.password = vmessBean.id
server.method = vmessBean.security
} else if (config.configType == EConfigType.SOCKS) {
if (TextUtils.isEmpty(vmessBean.security) && TextUtils.isEmpty(vmessBean.id)) {
server.users = null
} else {
val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = vmessBean.security
socksUsersBean.pass = vmessBean.id
server.users = listOf(socksUsersBean)
}
} else if (config.configType == EConfigType.TROJAN) {
server.password = vmessBean.id
}
}
config.outboundBean?.streamSettings?.let { streamSetting ->
val sni = streamSetting.populateTransportSettings(vmessBean.network, vmessBean.headerType,
vmessBean.requestHost, vmessBean.path, vmessBean.path, vmessBean.requestHost, vmessBean.path,
vmessBean.headerType, vmessBean.path)
val allowInsecure = if (vmessBean.allowInsecure.isBlank()) {
settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
} else {
vmessBean.allowInsecure.toBoolean()
}
var fingerprint = streamSetting.tlsSettings?.fingerprint
streamSetting.populateTlsSettings(vmessBean.streamSecurity, allowInsecure,
vmessBean.sni.ifBlank { sni }, fingerprint)
}
}
val key = MmkvManager.encodeServerConfig(vmessBean.guid, config)
if (index == angConfig.index) {
mainStorage?.encode(KEY_SELECTED_SERVER, key)
}
}
}
private fun migrateSubItemBean(angConfig: AngConfig) {
angConfig.subItem.forEach {
val subItem = SubscriptionItem()
subItem.remarks = it.remarks
subItem.url = it.url
subItem.enabled = it.enabled
subStorage?.encode(it.id, Gson().toJson(subItem))
}
}
/**
* import config form qrcode or...
*/
private fun importConfig(str: String?, subid: String, removedSelectedServer: ServerConfig?): Int {
try { try {
if (str == null || TextUtils.isEmpty(str)) { if (str == null || TextUtils.isEmpty(str)) {
return R.string.toast_none_data return R.string.toast_none_data
} }
val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) { //maybe sub
VmessFmt.parse(str) if (TextUtils.isEmpty(subid) && (str.startsWith(HTTP_PROTOCOL) || str.startsWith(HTTPS_PROTOCOL))) {
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) { MmkvManager.importUrlAsSubscription(str)
ShadowsocksFmt.parse(str) return 0
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
SocksFmt.parse(str)
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
TrojanFmt.parse(str)
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
VlessFmt.parse(str)
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
WireguardFmt.parse(str)
} else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme) || str.startsWith(HY2)) {
Hysteria2Fmt.parse(str)
} else {
null
} }
if (config == null) { var config: ServerConfig? = null
val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
config = ServerConfig.create(EConfigType.VMESS)
val streamSetting = config.outboundBean?.streamSettings ?: return -1
var fingerprint = streamSetting.tlsSettings?.fingerprint
if (!tryParseNewVmess(str, config, allowInsecure)) {
if (str.indexOf("?") > 0) {
if (!tryResolveVmess4Kitsunebi(str, config)) {
return R.string.toast_incorrect_protocol
}
} else {
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
result = Utils.decode(result)
if (TextUtils.isEmpty(result)) {
return R.string.toast_decoding_failed
}
val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java)
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
if (TextUtils.isEmpty(vmessQRCode.add)
|| TextUtils.isEmpty(vmessQRCode.port)
|| TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net)
) {
return R.string.toast_incorrect_protocol
}
config.remarks = vmessQRCode.ps
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = vmessQRCode.add
vnext.port = Utils.parseInt(vmessQRCode.port)
vnext.users[0].id = vmessQRCode.id
vnext.users[0].security = if (TextUtils.isEmpty(vmessQRCode.scy)) DEFAULT_SECURITY else vmessQRCode.scy
vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid)
}
val sni = streamSetting.populateTransportSettings(vmessQRCode.net, vmessQRCode.type, vmessQRCode.host,
vmessQRCode.path, vmessQRCode.path, vmessQRCode.host, vmessQRCode.path, vmessQRCode.type, vmessQRCode.path)
streamSetting.populateTlsSettings(vmessQRCode.tls, allowInsecure,
if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni, fingerprint)
}
}
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
config = ServerConfig.create(EConfigType.SHADOWSOCKS)
if (!tryResolveResolveSip002(str, config)) {
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
val indexSplit = result.indexOf("#")
if (indexSplit > 0) {
try {
config.remarks = Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
result = if (indexS > 0) {
Utils.decode(result.substring(0, indexS)) + result.substring(indexS, result.length)
} else {
Utils.decode(result)
}
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
val match = legacyPattern.matchEntire(result) ?: return R.string.toast_incorrect_protocol
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = match.groupValues[3].removeSurrounding("[", "]")
server.port = match.groupValues[4].toInt()
server.password = match.groupValues[2]
server.method = match.groupValues[1].lowercase()
}
}
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
var result = str.replace(EConfigType.SOCKS.protocolScheme, "")
val indexSplit = result.indexOf("#")
config = ServerConfig.create(EConfigType.SOCKS)
if (indexSplit > 0) {
try {
config.remarks = Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
if (indexS > 0) {
result = Utils.decode(result.substring(0, indexS)) + result.substring(indexS, result.length)
} else {
result = Utils.decode(result)
}
val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex()
val match = legacyPattern.matchEntire(result) ?: return R.string.toast_incorrect_protocol
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = match.groupValues[3].removeSurrounding("[", "]")
server.port = match.groupValues[4].toInt()
val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = match.groupValues[1].lowercase()
socksUsersBean.pass = match.groupValues[2]
server.users = listOf(socksUsersBean)
}
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
val uri = URI(Utils.fixIllegalUrl(str))
config = ServerConfig.create(EConfigType.TROJAN)
config.remarks = Utils.urlDecode(uri.fragment ?: "")
var flow = ""
var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint
if (uri.rawQuery != null) {
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val sni = config.outboundBean?.streamSettings?.populateTransportSettings(queryParam["type"] ?: "tcp", queryParam["headerType"],
queryParam["host"], queryParam["path"], queryParam["seed"], queryParam["quicSecurity"], queryParam["key"],
queryParam["mode"], queryParam["serviceName"])
config.outboundBean?.streamSettings?.populateTlsSettings(queryParam["security"] ?: TLS, allowInsecure, queryParam["sni"] ?: sni!!, fingerprint)
flow = queryParam["flow"] ?: ""
} else {
config.outboundBean?.streamSettings?.populateTlsSettings(TLS, allowInsecure, "", fingerprint)
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = uri.idnHost
server.port = uri.port
server.password = uri.userInfo
server.flow = flow
}
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
val uri = URI(Utils.fixIllegalUrl(str))
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
config = ServerConfig.create(EConfigType.VLESS)
val streamSetting = config.outboundBean?.streamSettings ?: return -1
var fingerprint = streamSetting.tlsSettings?.fingerprint
config.remarks = Utils.urlDecode(uri.fragment ?: "")
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = uri.idnHost
vnext.port = uri.port
vnext.users[0].id = uri.userInfo
vnext.users[0].encryption = queryParam["encryption"] ?: "none"
vnext.users[0].flow =queryParam["flow"] ?: ""
}
val sni = streamSetting.populateTransportSettings(queryParam["type"] ?: "tcp", queryParam["headerType"],
queryParam["host"], queryParam["path"], queryParam["seed"], queryParam["quicSecurity"], queryParam["key"],
queryParam["mode"], queryParam["serviceName"])
streamSetting.populateTlsSettings(queryParam["security"] ?: "", allowInsecure, queryParam["sni"] ?: sni, fingerprint)
}
if (config == null){
return R.string.toast_incorrect_protocol return R.string.toast_incorrect_protocol
} }
//filter
if (subItem?.filter != null && subItem.filter?.isNotEmpty() == true && config.remarks.isNotEmpty()) {
val matched = Regex(pattern = subItem.filter ?: "")
.containsMatchIn(input = config.remarks)
if (!matched) return -1
}
config.subscriptionId = subid config.subscriptionId = subid
val guid = MmkvManager.encodeServerConfig("", config) val guid = MmkvManager.encodeServerConfig("", config)
if (removedSelectedServer != null && if (removedSelectedServer != null &&
config.getProxyOutbound() config.getProxyOutbound()?.getServerAddress() == removedSelectedServer.getProxyOutbound()?.getServerAddress() &&
?.getServerAddress() == removedSelectedServer.getProxyOutbound() config.getProxyOutbound()?.getServerPort() == removedSelectedServer.getProxyOutbound()?.getServerPort()) {
?.getServerAddress() && mainStorage?.encode(KEY_SELECTED_SERVER, guid)
config.getProxyOutbound()
?.getServerPort() == removedSelectedServer.getProxyOutbound()
?.getServerPort()
) {
MmkvManager.setSelectServer(guid)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -87,23 +358,234 @@ object AngConfigManager {
return 0 return 0
} }
private fun tryParseNewVmess(uriString: String, config: ServerConfig, allowInsecure: Boolean): Boolean {
return runCatching {
val uri = URI(uriString)
check(uri.scheme == "vmess")
val (_, protocol, tlsStr, uuid, alterId) =
Regex("(tcp|http|ws|kcp|quic|grpc)(\\+tls)?:([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})")
.matchEntire(uri.userInfo)?.groupValues
?: error("parse user info fail.")
val tls = tlsStr.isNotBlank()
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val streamSetting = config.outboundBean?.streamSettings ?: return false
config.remarks = Utils.urlDecode(uri.fragment ?: "")
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = uri.idnHost
vnext.port = uri.port
vnext.users[0].id = uuid
vnext.users[0].security = DEFAULT_SECURITY
vnext.users[0].alterId = alterId.toInt()
}
var fingerprint = streamSetting.tlsSettings?.fingerprint
val sni = streamSetting.populateTransportSettings(protocol, queryParam["type"],
queryParam["host"]?.split("|")?.get(0) ?: "",
queryParam["path"]?.takeIf { it.trim() != "/" } ?: "", queryParam["seed"], queryParam["security"],
queryParam["key"], queryParam["mode"], queryParam["serviceName"])
streamSetting.populateTlsSettings(if (tls) TLS else "", allowInsecure, sni, fingerprint)
true
}.getOrElse { false }
}
private fun tryResolveVmess4Kitsunebi(server: String, config: ServerConfig): Boolean {
var result = server.replace(EConfigType.VMESS.protocolScheme, "")
val indexSplit = result.indexOf("?")
if (indexSplit > 0) {
result = result.substring(0, indexSplit)
}
result = Utils.decode(result)
val arr1 = result.split('@')
if (arr1.count() != 2) {
return false
}
val arr21 = arr1[0].split(':')
val arr22 = arr1[1].split(':')
if (arr21.count() != 2) {
return false
}
config.remarks = "Alien"
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = arr22[0]
vnext.port = Utils.parseInt(arr22[1])
vnext.users[0].id = arr21[1]
vnext.users[0].security = arr21[0]
vnext.users[0].alterId = 0
}
return true
}
private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {
try {
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment ?: "")
val method: String
val password: String
if (uri.userInfo.contains(":")) {
val arrUserInfo = uri.userInfo.split(":").map { it.trim() }
if (arrUserInfo.count() != 2) {
return false
}
method = arrUserInfo[0]
password = Utils.urlDecode(arrUserInfo[1])
} else {
val base64Decode = Utils.decode(uri.userInfo)
val arrUserInfo = base64Decode.split(":").map { it.trim() }
if (arrUserInfo.count() < 2) {
return false
}
method = arrUserInfo[0]
password = base64Decode.substringAfter(":")
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = uri.idnHost
server.port = uri.port
server.password = password
server.method = method
}
return true
} catch (e: Exception) {
Log.d(AppConfig.ANG_PACKAGE, e.toString())
return false
}
}
/** /**
* share config * 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 ?: return ""
return config.configType.protocolScheme + when (config.configType) { return config.configType.protocolScheme + when (config.configType) {
EConfigType.VMESS -> VmessFmt.toUri(config) EConfigType.VMESS -> {
val vmessQRCode = VmessQRCode()
vmessQRCode.v = "2"
vmessQRCode.ps = config.remarks
vmessQRCode.add = outbound.getServerAddress().orEmpty()
vmessQRCode.port = outbound.getServerPort().toString()
vmessQRCode.id = outbound.getPassword().orEmpty()
vmessQRCode.aid = outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()
vmessQRCode.scy = outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString()
vmessQRCode.net = streamSetting.network
vmessQRCode.tls = streamSetting.security
vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty()
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.HTTP -> "" val pw = Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
EConfigType.VLESS -> VlessFmt.toUri(config) val url = String.format("%s@%s:%s",
EConfigType.TROJAN -> TrojanFmt.toUri(config) pw,
EConfigType.WIREGUARD -> WireguardFmt.toUri(config) Utils.getIpv6Address(outbound.getServerAddress()!!),
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config) outbound.getServerPort())
url + remark
}
EConfigType.SOCKS -> {
val remark = "#" + Utils.urlEncode(config.remarks)
val pw = Utils.encode("${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}")
val url = String.format("%s@%s:%s",
pw,
Utils.getIpv6Address(outbound.getServerAddress()!!),
outbound.getServerPort())
url + remark
}
EConfigType.VLESS,
EConfigType.TROJAN -> {
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<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.xtlsSettings)?.let { tlsSetting ->
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
dicQuery["sni"] = tlsSetting.serverName
}
}
dicQuery["type"] = streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
outbound.getTransportSettingDetails()?.let { transportDetails ->
when (streamSetting.network) {
"tcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
}
"kcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
}
}
"ws" -> {
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"http", "h2" -> {
dicQuery["type"] = "http"
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"quic" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
}
"grpc" -> {
dicQuery["mode"] = transportDetails[0]
dicQuery["serviceName"] = transportDetails[2]
}
}
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format("%s@%s:%s",
outbound.getPassword(),
Utils.getIpv6Address(outbound.getServerAddress()!!),
outbound.getServerPort())
url + query + remark
}
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -163,7 +645,7 @@ object AngConfigManager {
if (TextUtils.isEmpty(conf)) { if (TextUtils.isEmpty(conf)) {
return null return null
} }
return QRCodeDecoder.createQRCode(conf) return Utils.createQRCode(conf)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -190,225 +672,75 @@ object AngConfigManager {
return 0 return 0
} }
fun importBatchConfig(server: String?, subid: String, append: Boolean): Pair<Int, Int> { /**
var count = parseBatchConfig(Utils.decode(server), subid, append) * upgrade
if (count <= 0) { */
count = parseBatchConfig(server, subid, append) private fun upgradeServerVersion(vmess: AngConfig.VmessBean): Int {
}
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 { try {
if (servers == null) { if (vmess.configVersion == 2) {
return 0 return 0
} }
var count = 0 when (vmess.network) {
servers.lines() "ws", "h2" -> {
.forEach { str -> var path = ""
if (str.startsWith(AppConfig.PROTOCOL_HTTP) || str.startsWith(AppConfig.PROTOCOL_HTTPS)) { var host = ""
count += importUrlAsSubscription(str) val lstParameter = vmess.requestHost.split(";")
if (lstParameter.isNotEmpty()) {
path = lstParameter[0].trim()
} }
if (lstParameter.size > 1) {
path = lstParameter[0].trim()
host = lstParameter[1].trim()
}
vmess.path = path
vmess.requestHost = host
} }
return count }
vmess.configVersion = 2
return 0
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return -1
} }
return 0
} }
fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int { fun importBatchConfig(servers: String?, subid: String, append: Boolean): Int {
try { try {
if (servers == null) { if (servers == null) {
return 0 return 0
} }
val removedSelectedServer = val removedSelectedServer =
if (!TextUtils.isEmpty(subid) && !append) { if (!TextUtils.isEmpty(subid) && !append) {
MmkvManager.decodeServerConfig( MmkvManager.decodeServerConfig(mainStorage?.decodeString(KEY_SELECTED_SERVER) ?: "")?.let {
MmkvManager.getSelectServer().orEmpty() if (it.subscriptionId == subid) {
)?.let { return@let it
if (it.subscriptionId == subid) { }
return@let it return@let null
} }
return@let null } else {
null
} }
} else { if(!append) {
null
}
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", "")
// }
val subItem = MmkvManager.decodeSubscription(subid)
var count = 0 var count = 0
servers.lines() servers.lines()
.reversed() .forEach {
.forEach { val resId = importConfig(it, subid, removedSelectedServer)
val resId = parseConfig(it, subid, subItem, removedSelectedServer) if (resId == 0) {
if (resId == 0) { count++
count++ }
} }
}
return count return count
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
return 0 return 0
} }
fun parseCustomConfigServer(server: String?, subid: String): Int {
if (server == null) {
return 0
}
if (server.contains("inbounds")
&& server.contains("outbounds")
&& server.contains("routing")
) {
try {
val serverList: Array<Any> =
JsonUtil.fromJson(server, Array<Any>::class.java)
if (serverList.isNotEmpty()) {
var count = 0
for (srv in serverList.reversed()) {
val config = ServerConfig.create(EConfigType.CUSTOM)
config.fullConfig =
JsonUtil.fromJson(JsonUtil.toJson(srv), V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks
?: ("%04d-".format(count + 1) + System.currentTimeMillis()
.toString())
config.subscriptionId = subid
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv))
count += 1
}
return count
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
// For compatibility
val config = ServerConfig.create(EConfigType.CUSTOM)
config.subscriptionId = subid
config.fullConfig = JsonUtil.fromJson(server, V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
return 1
} catch (e: Exception) {
e.printStackTrace()
}
return 0
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
try {
val config = WireguardFmt.parseWireguardConfFile(server)
?: return R.string.toast_incorrect_protocol
config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
return 1
} catch (e: Exception) {
e.printStackTrace()
}
return 0
} else {
return 0
}
}
fun updateConfigViaSubAll(): Int {
var count = 0
try {
MmkvManager.decodeSubscriptions().forEach {
count += updateConfigViaSub(it)
}
} catch (e: Exception) {
e.printStackTrace()
return 0
}
return count
}
fun updateConfigViaSub(it: Pair<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 {
val httpPort = SettingsManager.getHttpPort()
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
} catch (e: Exception) {
e.printStackTrace()
""
}
if (configText.isEmpty()) {
configText = try {
Utils.getUrlContentWithCustomUserAgent(url)
} catch (e: Exception) {
e.printStackTrace()
""
}
}
if (configText.isEmpty()) {
return 0
}
return parseConfigViaSub(configText, it.first, false)
} catch (e: Exception) {
e.printStackTrace()
return 0
}
}
private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int {
var count = parseBatchConfig(Utils.decode(server), subid, append)
if (count <= 0) {
count = parseBatchConfig(server, subid, append)
}
if (count <= 0) {
count = parseCustomConfigServer(server, subid)
}
return count
}
private fun importUrlAsSubscription(url: String): Int {
val subscriptions = MmkvManager.decodeSubscriptions()
subscriptions.forEach {
if (it.second.url == url) {
return 0
}
}
val uri = URI(Utils.fixIllegalUrl(url))
val subItem = SubscriptionItem()
subItem.remarks = uri.fragment ?: "import sub"
subItem.url = url
MmkvManager.encodeSubscription("", subItem)
return 1
}
} }
@@ -1,25 +1,28 @@
package com.v2ray.ang.util package com.v2ray.ang.util
import android.Manifest
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
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 io.reactivex.rxjava3.core.Observable import rx.Observable
import java.util.*
object AppManagerUtil { object AppManagerUtil {
private fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> { fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> {
val packageManager = ctx.packageManager val packageManager = ctx.packageManager
val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS) val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
val apps = ArrayList<AppInfo>() val apps = ArrayList<AppInfo>()
for (pkg in packages) { for (pkg in packages) {
//if (!pkg.hasInternetPermission && pkg.packageName != "android") continue if (!pkg.hasInternetPermission && pkg.packageName != "android") continue
val applicationInfo = pkg.applicationInfo val applicationInfo = pkg.applicationInfo
val appName = applicationInfo.loadLabel(packageManager).toString() val appName = applicationInfo.loadLabel(packageManager).toString()
val appIcon = applicationInfo.loadIcon(packageManager) val appIcon = applicationInfo.loadIcon(packageManager)
val isSystemApp = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM > 0 val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0) val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
apps.add(appInfo) apps.add(appInfo)
@@ -28,14 +31,13 @@ object AppManagerUtil {
return apps return apps
} }
fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> = fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> = Observable.unsafeCreate {
Observable.unsafeCreate { it.onNext(loadNetworkAppList(ctx))
it.onNext(loadNetworkAppList(ctx)) }
}
// val PackageInfo.hasInternetPermission: Boolean val PackageInfo.hasInternetPermission: Boolean
// get() { get() {
// val permissions = requestedPermissions val permissions = requestedPermissions
// return permissions?.any { it == Manifest.permission.INTERNET } ?: false return permissions?.any { it == Manifest.permission.INTERNET } ?: false
// } }
} }
@@ -1,37 +0,0 @@
package com.v2ray.ang.util
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
object JsonUtil {
private var gson = Gson()
fun toJson(src: Any?): String {
return gson.toJson(src)
}
fun <T> fromJson(json: String, cls: Class<T>): T {
return gson.fromJson(json, cls)
}
fun toJsonPretty(src: Any?): String {
val gsonPre = GsonBuilder()
.setPrettyPrinting()
.disableHtmlEscaping()
.registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
object : TypeToken<Double>() {}.type,
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? ->
JsonPrimitive(
src?.toInt()
)
}
)
.create()
return gsonPre.toJson(src)
}
}
@@ -1,63 +1,32 @@
package com.v2ray.ang.util package com.v2ray.ang.util
import com.google.gson.Gson
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServerAffiliationInfo import com.v2ray.ang.dto.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
object MmkvManager { object MmkvManager {
const val ID_MAIN = "MAIN"
//region private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
const val ID_SERVER_RAW = "SERVER_RAW"
private const val ID_MAIN = "MAIN" const val ID_SERVER_AFF = "SERVER_AFF"
private const val ID_SERVER_CONFIG = "SERVER_CONFIG" const val ID_SUB = "SUB"
private const val ID_PROFILE_CONFIG = "PROFILE_CONFIG" const val ID_SETTING = "SETTING"
private const val ID_SERVER_RAW = "SERVER_RAW" const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
private const val ID_SERVER_AFF = "SERVER_AFF" const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
private const val ID_SUB = "SUB"
private const val ID_ASSET = "ASSET"
private const val ID_SETTING = "SETTING"
private const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
private const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
private const val KEY_SUB_IDS = "SUB_IDS"
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) } private 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) } private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
//endregion
//region Server
fun getSelectServer(): String? {
return mainStorage.decodeString(KEY_SELECTED_SERVER)
}
fun setSelectServer(guid: String) {
mainStorage.encode(KEY_SELECTED_SERVER, guid)
}
fun encodeServerList(serverList: MutableList<String>) {
mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
}
fun decodeServerList(): MutableList<String> { fun decodeServerList(): MutableList<String> {
val json = mainStorage.decodeString(KEY_ANG_CONFIGS) val json = mainStorage?.decodeString(KEY_ANG_CONFIGS)
return if (json.isNullOrBlank()) { return if (json.isNullOrBlank()) {
mutableListOf() mutableListOf()
} else { } else {
JsonUtil.fromJson(json, Array<String>::class.java).toMutableList() Gson().fromJson(json, Array<String>::class.java).toMutableList()
} }
} }
@@ -65,43 +34,24 @@ object MmkvManager {
if (guid.isBlank()) { if (guid.isBlank()) {
return null return null
} }
val json = serverStorage.decodeString(guid) val json = serverStorage?.decodeString(guid)
if (json.isNullOrBlank()) { if (json.isNullOrBlank()) {
return null return null
} }
return JsonUtil.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 JsonUtil.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, JsonUtil.toJson(config)) serverStorage?.encode(key, Gson().toJson(config))
val serverList = decodeServerList() val serverList = decodeServerList()
if (!serverList.contains(key)) { if (!serverList.contains(key)) {
serverList.add(0, key) serverList.add(key)
encodeServerList(serverList) mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
if (getSelectServer().isNullOrBlank()) { if (mainStorage?.decodeString(KEY_SELECTED_SERVER).isNullOrBlank()) {
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, JsonUtil.toJson(profile))
return key return key
} }
@@ -109,22 +59,21 @@ object MmkvManager {
if (guid.isBlank()) { if (guid.isBlank()) {
return return
} }
if (getSelectServer() == guid) { if (mainStorage?.decodeString(KEY_SELECTED_SERVER) == guid) {
mainStorage.remove(KEY_SELECTED_SERVER) mainStorage?.remove(KEY_SELECTED_SERVER)
} }
val serverList = decodeServerList() val serverList = decodeServerList()
serverList.remove(guid) serverList.remove(guid)
encodeServerList(serverList) mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
serverStorage.remove(guid) serverStorage?.remove(guid)
profileStorage.remove(guid) serverAffStorage?.remove(guid)
serverAffStorage.remove(guid)
} }
fun removeServerViaSubid(subid: String) { fun removeServerViaSubid(subid: String) {
if (subid.isBlank()) { if (subid.isBlank()) {
return return
} }
serverStorage.allKeys()?.forEach { key -> serverStorage?.allKeys()?.forEach { key ->
decodeServerConfig(key)?.let { config -> decodeServerConfig(key)?.let { config ->
if (config.subscriptionId == subid) { if (config.subscriptionId == subid) {
removeServer(key) removeServer(key)
@@ -137,11 +86,11 @@ object MmkvManager {
if (guid.isBlank()) { if (guid.isBlank()) {
return null return null
} }
val json = serverAffStorage.decodeString(guid) val json = serverAffStorage?.decodeString(guid)
if (json.isNullOrBlank()) { if (json.isNullOrBlank()) {
return null return null
} }
return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java) return Gson().fromJson(json, ServerAffiliationInfo::class.java)
} }
fun encodeServerTestDelayMillis(guid: String, testResult: Long) { fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
@@ -150,175 +99,81 @@ object MmkvManager {
} }
val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo() val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo()
aff.testDelayMillis = testResult aff.testDelayMillis = testResult
serverAffStorage.encode(guid, JsonUtil.toJson(aff)) serverAffStorage?.encode(guid, Gson().toJson(aff))
} }
fun clearAllTestDelayResults(keys: List<String>?) { fun clearAllTestDelayResults() {
keys?.forEach { key -> serverAffStorage?.allKeys()?.forEach { key ->
decodeServerAffiliationInfo(key)?.let { aff -> decodeServerAffiliationInfo(key)?.let { aff ->
aff.testDelayMillis = 0 aff.testDelayMillis = 0
serverAffStorage.encode(key, JsonUtil.toJson(aff)) serverAffStorage?.encode(key, Gson().toJson(aff))
} }
} }
} }
fun removeAllServer() { fun importUrlAsSubscription(url: String): Int {
mainStorage.clearAll() val subscriptions = decodeSubscriptions()
serverStorage.clearAll() subscriptions.forEach {
profileStorage.clearAll() if (it.second.url == url) {
serverAffStorage.clearAll() return 0
}
fun removeInvalidServer(guid: String) {
if (guid.isNotEmpty()) {
decodeServerAffiliationInfo(guid)?.let { aff ->
if (aff.testDelayMillis < 0L) {
removeServer(guid)
}
}
} else {
serverAffStorage.allKeys()?.forEach { key ->
decodeServerAffiliationInfo(key)?.let { aff ->
if (aff.testDelayMillis < 0L) {
removeServer(key)
}
}
} }
} }
} val subItem = SubscriptionItem()
subItem.remarks = "import sub"
fun encodeServerRaw(guid: String, config: String) { subItem.url = url
serverRawStorage.encode(guid, config) subStorage?.encode(Utils.getUuid(), Gson().toJson(subItem))
} return 1
fun decodeServerRaw(guid: String): String? {
return serverRawStorage.decodeString(guid) ?: return null
}
//endregion
//region Subscriptions
private fun initSubsList() {
val subsList = decodeSubsList()
if (subsList.isNotEmpty()) {
return
}
subStorage.allKeys()?.forEach { key ->
subsList.add(key)
}
encodeSubsList(subsList)
} }
fun decodeSubscriptions(): List<Pair<String, SubscriptionItem>> { fun decodeSubscriptions(): List<Pair<String, SubscriptionItem>> {
initSubsList()
val subscriptions = mutableListOf<Pair<String, SubscriptionItem>>() val subscriptions = mutableListOf<Pair<String, SubscriptionItem>>()
decodeSubsList().forEach { key -> subStorage?.allKeys()?.forEach { key ->
val json = subStorage.decodeString(key) val json = subStorage?.decodeString(key)
if (!json.isNullOrBlank()) { if (!json.isNullOrBlank()) {
subscriptions.add(Pair(key, JsonUtil.fromJson(json, SubscriptionItem::class.java))) subscriptions.add(Pair(key, Gson().fromJson(json, SubscriptionItem::class.java)))
} }
} }
subscriptions.sortedBy { (_, value) -> value.addedTime }
return subscriptions return subscriptions
} }
fun removeSubscription(subid: String) { fun removeSubscription(subid: String) {
subStorage.remove(subid) subStorage?.remove(subid)
val subsList = decodeSubsList()
subsList.remove(subid)
encodeSubsList(subsList)
removeServerViaSubid(subid) removeServerViaSubid(subid)
} }
fun encodeSubscription(guid: String, subItem: SubscriptionItem) { fun removeAllServer() {
val key = guid.ifBlank { Utils.getUuid() } mainStorage?.clearAll()
subStorage.encode(key, JsonUtil.toJson(subItem)) serverStorage?.clearAll()
serverAffStorage?.clearAll()
val subsList = decodeSubsList()
if (!subsList.contains(key)) {
subsList.add(key)
encodeSubsList(subsList)
}
} }
fun decodeSubscription(subscriptionId: String): SubscriptionItem? { fun removeInvalidServer() {
val json = subStorage.decodeString(subscriptionId) ?: return null serverAffStorage?.allKeys()?.forEach { key ->
return JsonUtil.fromJson(json, SubscriptionItem::class.java) decodeServerAffiliationInfo(key)?.let { aff ->
} if (aff.testDelayMillis <= 0L) {
removeServer(key)
fun encodeSubsList(subsList: MutableList<String>) { }
mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList))
}
fun decodeSubsList(): MutableList<String> {
val json = mainStorage.decodeString(KEY_SUB_IDS)
return if (json.isNullOrBlank()) {
mutableListOf()
} else {
JsonUtil.fromJson(json, Array<String>::class.java).toMutableList()
}
}
//endregion
//region Asset
fun decodeAssetUrls(): List<Pair<String, AssetUrlItem>> {
val assetUrlItems = mutableListOf<Pair<String, AssetUrlItem>>()
assetStorage.allKeys()?.forEach { key ->
val json = assetStorage.decodeString(key)
if (!json.isNullOrBlank()) {
assetUrlItems.add(Pair(key, JsonUtil.fromJson(json, AssetUrlItem::class.java)))
} }
} }
return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
} }
fun removeAssetUrl(assetid: String) { fun sortByTestResults( ) {
assetStorage.remove(assetid) data class ServerDelay(var guid: String, var testDelayMillis: Long)
val serverDelays = mutableListOf<ServerDelay>()
val serverList = decodeServerList()
serverList.forEach { key ->
val delay = decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L
serverDelays.add(ServerDelay(key, if (delay <= 0L) 999999 else delay))
}
serverDelays.sortBy { it.testDelayMillis }
serverDelays.forEach {
serverList.remove(it.guid)
serverList.add(it.guid)
}
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
} }
fun encodeAsset(assetid: String, assetItem: AssetUrlItem) {
val key = assetid.ifBlank { Utils.getUuid() }
assetStorage.encode(key, JsonUtil.toJson(assetItem))
}
fun decodeAsset(assetid: String): AssetUrlItem? {
val json = assetStorage.decodeString(assetid) ?: return null
return JsonUtil.fromJson(json, AssetUrlItem::class.java)
}
//endregion
//region Routing
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
if (ruleset.isNullOrEmpty()) return null
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java).toMutableList()
}
fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) {
if (rulesetList.isNullOrEmpty())
settingsStorage.encode(PREF_ROUTING_RULESET, "")
else
settingsStorage.encode(PREF_ROUTING_RULESET, JsonUtil.toJson(rulesetList))
}
//endregion
//region Others
fun encodeStartOnBoot(startOnBoot: Boolean) {
settingsStorage.encode(PREF_IS_BOOTED, startOnBoot)
}
fun decodeStartOnBoot(): Boolean {
return settingsStorage.decodeBool(PREF_IS_BOOTED, false)
}
//endregion
} }
@@ -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.Locale import java.util.*
open class MyContextWrapper(base: Context?) : ContextWrapper(base) { open class MyContextWrapper(base: Context?) : ContextWrapper(base) {
companion object { companion object {
@@ -1,99 +0,0 @@
package com.v2ray.ang.util
import android.content.Context
import android.os.SystemClock
import android.util.Log
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.service.ProcessService
import com.v2ray.ang.util.fmt.Hysteria2Fmt
import java.io.File
object PluginUtil {
//private const val HYSTERIA2 = "hysteria2-plugin"
private const val HYSTERIA2 = "libhysteria2.so"
private const val TAG = ANG_PACKAGE
private lateinit var procService: ProcessService
// fun initPlugin(name: String): PluginManager.InitResult {
// return PluginManager.init(name)!!
// }
fun runPlugin(context: Context, config: ServerConfig?, domainPort: String?) {
Log.d(TAG, "runPlugin")
val outbound = config?.getProxyOutbound() ?: return
if (outbound.protocol.equals(EConfigType.HYSTERIA2.name, true)) {
val configFile = genConfigHy2(context, config, domainPort) ?: return
val cmd = genCmdHy2(context, configFile)
procService = ProcessService()
procService.runProcess(context, cmd)
}
}
fun stopPlugin() {
stopHy2()
}
fun realPingHy2(context: Context, config: ServerConfig?): Long {
Log.d(TAG, "realPingHy2")
val retFailure = -1L
val outbound = config?.getProxyOutbound() ?: return retFailure
if (outbound.protocol.equals(EConfigType.HYSTERIA2.name, true)) {
val socksPort = Utils.findFreePort(listOf(0))
val configFile = genConfigHy2(context, config, "0:${socksPort}") ?: return retFailure
val cmd = genCmdHy2(context, configFile)
val proc = ProcessService()
proc.runProcess(context, cmd)
Thread.sleep(1000L)
val delay = SpeedtestUtil.testConnection(context, socksPort)
proc.stopProcess()
return delay.first
}
return retFailure
}
private fun genConfigHy2(context: Context, config: ServerConfig, domainPort: String?): File? {
Log.d(TAG, "runPlugin $HYSTERIA2")
val socksPort = domainPort?.split(":")?.last()
.let { if (it.isNullOrEmpty()) return null else it.toInt() }
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null
val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
Log.d(TAG, "runPlugin ${configFile.absolutePath}")
configFile.parentFile?.mkdirs()
configFile.writeText(JsonUtil.toJson(hy2Config))
Log.d(TAG, JsonUtil.toJson(hy2Config))
return configFile
}
private fun genCmdHy2(context: Context, configFile: File): MutableList<String> {
return mutableListOf(
File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath,
//initPlugin(HYSTERIA2).path,
"--disable-update-check",
"--config",
configFile.absolutePath,
"--log-level",
"warn",
"client"
)
}
private fun stopHy2() {
try {
Log.d(TAG, "$HYSTERIA2 destroy")
procService?.stopProcess()
} catch (e: Exception) {
Log.d(TAG, e.toString())
}
}
}
@@ -2,16 +2,10 @@ 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.BarcodeFormat import com.google.zxing.*
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.qrcode.QRCodeReader import com.google.zxing.common.HybridBinarizer
import com.google.zxing.qrcode.QRCodeWriter import java.util.*
import java.util.EnumMap
/** /**
* 描述:解析二维码图片 * 描述:解析二维码图片
@@ -19,40 +13,6 @@ import java.util.EnumMap
object QRCodeDecoder { object QRCodeDecoder {
val HINTS: MutableMap<DecodeHintType, Any?> = EnumMap(DecodeHintType::class.java) val HINTS: MutableMap<DecodeHintType, Any?> = EnumMap(DecodeHintType::class.java)
/**
* create qrcode using zxing
*/
fun createQRCode(text: String, size: Int = 800): Bitmap? {
try {
val hints = HashMap<EncodeHintType, String>()
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
val bitMatrix = QRCodeWriter().encode(
text,
BarcodeFormat.QR_CODE, size, size, hints
)
val pixels = IntArray(size * size)
for (y in 0 until size) {
for (x in 0 until size) {
if (bitMatrix.get(x, y)) {
pixels[y * size + x] = 0xff000000.toInt()
} else {
pixels[y * size + x] = 0xffffffff.toInt()
}
}
}
val bitmap = Bitmap.createBitmap(
size, size,
Bitmap.Config.ARGB_8888
)
bitmap.setPixels(pixels, 0, size, 0, 0, size, size)
return bitmap
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
/** /**
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。 * 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
* *
@@ -70,37 +30,24 @@ 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)
val qrReader = QRCodeReader() return MultiFormatReader().decode(BinaryBitmap(HybridBinarizer(source)), HINTS).text
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) {
e.printStackTrace()
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
if (source != null) {
try {
return MultiFormatReader().decode(BinaryBitmap(GlobalHistogramBinarizer(source)), HINTS).text
} catch (e2: Throwable) {
e2.printStackTrace()
}
}
return null return null
} }
@@ -129,24 +76,23 @@ 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"
@@ -1,160 +0,0 @@
package com.v2ray.ang.util
import android.content.Context
import android.text.TextUtils
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.util.MmkvManager.decodeProfileConfig
import com.v2ray.ang.util.MmkvManager.decodeServerConfig
import com.v2ray.ang.util.MmkvManager.decodeServerList
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils.parseInt
import java.util.Collections
object SettingsManager {
fun initRoutingRulesets(context: Context) {
val exist = MmkvManager.decodeRoutingRulesets()
if (exist.isNullOrEmpty()) {
val rulesetList = getPresetRoutingRulesets(context)
MmkvManager.encodeRoutingRulesets(rulesetList)
}
}
private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList<RulesetItem>? {
val fileName = when (index) {
0 -> "custom_routing_white"
1 -> "custom_routing_black"
2 -> "custom_routing_global"
3 -> "custom_routing_white_iran"
else -> "custom_routing_white"
}
val assets = Utils.readTextFromAssets(context, fileName)
if (TextUtils.isEmpty(assets)) {
return null
}
return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java).toMutableList()
}
fun resetRoutingRulesets(context: Context, index: Int) {
val rulesetList = getPresetRoutingRulesets(context, index) ?: return
resetRoutingRulesetsCommon(rulesetList)
}
fun resetRoutingRulesetsFromClipboard(content: String?): Boolean {
if (content.isNullOrEmpty()) {
return false
}
try {
val rulesetList = JsonUtil.fromJson(content, Array<RulesetItem>::class.java).toMutableList()
if (rulesetList.isNullOrEmpty()) {
return false
}
resetRoutingRulesetsCommon(rulesetList)
return true
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
private fun resetRoutingRulesetsCommon(rulesetList: MutableList<RulesetItem>) {
val rulesetNew: MutableList<RulesetItem> = mutableListOf()
MmkvManager.decodeRoutingRulesets()?.forEach { key ->
if (key.looked == true) {
rulesetNew.add(key)
}
}
rulesetNew.addAll(rulesetList)
MmkvManager.encodeRoutingRulesets(rulesetNew)
}
fun getRoutingRuleset(index: Int): RulesetItem? {
if (index < 0) return null
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) return null
return rulesetList[index]
}
fun saveRoutingRuleset(index: Int, ruleset: RulesetItem?) {
if (ruleset == null) return
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) return
if (index < 0 || index >= rulesetList.count()) {
rulesetList.add(ruleset)
} else {
rulesetList[index] = ruleset
}
MmkvManager.encodeRoutingRulesets(rulesetList)
}
fun removeRoutingRuleset(index: Int) {
if (index < 0) return
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) return
rulesetList.removeAt(index)
MmkvManager.encodeRoutingRulesets(rulesetList)
}
fun routingRulesetsBypassLan(): Boolean {
val rulesetItems = MmkvManager.decodeRoutingRulesets()
val exist = rulesetItems?.filter { it.enabled && it.outboundTag == TAG_DIRECT }?.any {
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
}
return exist == true
}
fun swapRoutingRuleset(fromPosition: Int, toPosition: Int) {
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) return
Collections.swap(rulesetList, fromPosition, toPosition)
MmkvManager.encodeRoutingRulesets(rulesetList)
}
fun swapSubscriptions(fromPosition: Int, toPosition: Int) {
val subsList = MmkvManager.decodeSubsList()
if (subsList.isNullOrEmpty()) return
Collections.swap(subsList, fromPosition, toPosition)
MmkvManager.encodeSubsList(subsList)
}
fun getServerViaRemarks(remarks: String?): ServerConfig? {
if (remarks == null) {
return null
}
val serverList = decodeServerList()
for (guid in serverList) {
val profile = decodeProfileConfig(guid)
if (profile != null && profile.remarks == remarks) {
return decodeServerConfig(guid)
}
}
return null
}
fun getSocksPort(): Int {
return parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
}
fun getHttpPort(): Int {
return parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
}
}
@@ -5,18 +5,13 @@ import android.os.SystemClock
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.extension.responseLength 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.HttpURLConnection import java.net.*
import java.net.InetSocketAddress import java.util.*
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 {
@@ -39,7 +34,7 @@ object SpeedtestUtil {
fun realPing(config: String): Long { fun realPing(config: String): Long {
return try { return try {
Libv2ray.measureOutboundDelay(config, Utils.getDelayTestUrl()) Libv2ray.measureOutboundDelay(config)
} catch (e: Exception) { } catch (e: Exception) {
Log.d(AppConfig.ANG_PACKAGE, "realPing: $e") Log.d(AppConfig.ANG_PACKAGE, "realPing: $e")
-1L -1L
@@ -53,8 +48,7 @@ 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 = val temps = tempInfo.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
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"
} }
@@ -72,7 +66,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,20 +92,19 @@ object SpeedtestUtil {
} }
} }
fun testConnection(context: Context, port: Int): Pair<Long, String> { fun testConnection(context: Context, port: Int): String {
// return V2RayVpnService.measureV2rayDelay()
var result: String var result: String
var elapsed = -1L
var conn: HttpURLConnection? = null var conn: HttpURLConnection? = null
try { try {
val url = URL(Utils.getDelayTestUrl()) val url = URL("https",
"www.google.com",
"/generate_204")
conn = url.openConnection( conn = url.openConnection(
Proxy( Proxy(Proxy.Type.HTTP,
Proxy.Type.HTTP, InetSocketAddress("127.0.0.1", port))) as HttpURLConnection
InetSocketAddress(LOOPBACK, port)
)
) as HttpURLConnection
conn.connectTimeout = 30000 conn.connectTimeout = 30000
conn.readTimeout = 30000 conn.readTimeout = 30000
conn.setRequestProperty("Connection", "close") conn.setRequestProperty("Connection", "close")
@@ -120,17 +113,12 @@ object SpeedtestUtil {
val start = SystemClock.elapsedRealtime() val start = SystemClock.elapsedRealtime()
val code = conn.responseCode val code = conn.responseCode
elapsed = SystemClock.elapsedRealtime() - start val elapsed = SystemClock.elapsedRealtime() - start
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( throw IOException(context.getString(R.string.connection_test_error_status_code, code))
context.getString(
R.string.connection_test_error_status_code,
code
)
)
} }
} catch (e: IOException) { } catch (e: IOException) {
// network exception // network exception
@@ -144,7 +132,7 @@ object SpeedtestUtil {
conn?.disconnect() conn?.disconnect()
} }
return Pair(elapsed, result) return result
} }
fun getLibVersion(): String { fun getLibVersion(): String {
@@ -1,44 +1,49 @@
package com.v2ray.ang.util package com.v2ray.ang.util
import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.text.Editable
import android.util.Base64
import com.google.zxing.WriterException
import android.graphics.Bitmap
import com.google.zxing.BarcodeFormat
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.EncodeHintType
import java.util.*
import kotlin.collections.HashMap
import android.content.ClipData
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.LocaleList import android.os.LocaleList
import android.provider.Settings
import android.text.Editable
import android.util.Base64
import android.util.Log import android.util.Log
import android.util.Patterns import android.util.Patterns
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.appcompat.app.AppCompatDelegate 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.ANG_PACKAGE
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import java.io.IOException
import java.net.* import java.net.*
import java.util.* import com.v2ray.ang.service.V2RayServiceManager
import java.io.IOException
object Utils { object Utils {
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
/** /**
* convert string to editalbe for kotlin * convert string to editalbe for kotlin
* *
* @param text * @param text
* @return * @return
*/ */
fun getEditable(text: String?): Editable { fun getEditable(text: String): Editable {
return Editable.Factory.getInstance().newEditable(text.orEmpty()) return Editable.Factory.getInstance().newEditable(text)
} }
/** /**
@@ -61,10 +66,15 @@ object Utils {
} }
fun parseInt(str: String?, default: Int): Int { fun parseInt(str: String?, default: Int): Int {
return str?.toIntOrNull() ?: default str ?: return default
return try {
Integer.parseInt(str)
} catch (e: Exception) {
e.printStackTrace()
default
}
} }
/** /**
* get text from clipboard * get text from clipboard
*/ */
@@ -94,19 +104,23 @@ object Utils {
/** /**
* base64 decode * base64 decode
*/ */
fun decode(text: String?): String { fun decode(text: String): String {
return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) }.orEmpty() tryDecodeBase64(text)?.let { return it }
if (text.endsWith('=')) {
// try again for some loosely formatted base64
tryDecodeBase64(text.trimEnd('='))?.let { return it }
}
return ""
} }
fun tryDecodeBase64(text: String): String? {
fun tryDecodeBase64(text: String?): String? {
try { try {
return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8) return Base64.decode(text, Base64.NO_WRAP).toString(charset("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(Charsets.UTF_8) return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(charset("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")
} }
@@ -118,7 +132,7 @@ object Utils {
*/ */
fun encode(text: String): String { fun encode(text: String): String {
return try { return try {
Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP) Base64.encodeToString(text.toByteArray(charset("UTF-8")), Base64.NO_WRAP)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
"" ""
@@ -129,16 +143,18 @@ object Utils {
* get remote dns servers from preference * get remote dns servers from preference
*/ */
fun getRemoteDnsServers(): List<String> { fun getRemoteDnsServers(): List<String> {
val remoteDns = settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY val remoteDns = settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_AGENT
val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) } val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
if (ret.isEmpty()) { if (ret.isEmpty()) {
return listOf(AppConfig.DNS_PROXY) return listOf(AppConfig.DNS_AGENT)
} }
return ret return ret
} }
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)
?: settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS)
?: AppConfig.DNS_AGENT
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
} }
@@ -155,6 +171,36 @@ object Utils {
return ret return ret
} }
/**
* create qrcode using zxing
*/
fun createQRCode(text: String, size: Int = 800): Bitmap? {
try {
val hints = HashMap<EncodeHintType, String>()
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
val bitMatrix = QRCodeWriter().encode(text,
BarcodeFormat.QR_CODE, size, size, hints)
val pixels = IntArray(size * size)
for (y in 0 until size) {
for (x in 0 until size) {
if (bitMatrix.get(x, y)) {
pixels[y * size + x] = 0xff000000.toInt()
} else {
pixels[y * size + x] = 0xffffffff.toInt()
}
}
}
val bitmap = Bitmap.createBitmap(size, size,
Bitmap.Config.ARGB_8888)
bitmap.setPixels(pixels, 0, size, 0, 0, size, size)
return bitmap
} catch (e: WriterException) {
e.printStackTrace()
return null
}
}
/** /**
* is ip address * is ip address
*/ */
@@ -167,7 +213,7 @@ object Utils {
//CIDR //CIDR
if (addr.indexOf("/") > 0) { if (addr.indexOf("/") > 0) {
val arr = addr.split("/") val arr = addr.split("/")
if (arr.count() == 2 && Integer.parseInt(arr[1]) > -1) { if (arr.count() == 2 && Integer.parseInt(arr[1]) > 0) {
addr = arr[0] addr = arr[0]
} }
} }
@@ -198,12 +244,11 @@ object Utils {
} }
fun isPureIpAddress(value: String): Boolean { fun isPureIpAddress(value: String): Boolean {
return isIpv4Address(value) || isIpv6Address(value) return (isIpv4Address(value) || isIpv6Address(value))
} }
fun isIpv4Address(value: String): Boolean { fun isIpv4Address(value: String): Boolean {
val regV4 = 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])$")
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)
} }
@@ -213,13 +258,12 @@ 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 = 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}$")
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") || s == "localhost" return s.startsWith("https") || s.startsWith("tcp") || s.startsWith("quic")
} }
/** /**
@@ -227,16 +271,10 @@ object Utils {
*/ */
fun isValidUrl(value: String?): Boolean { fun isValidUrl(value: String?): Boolean {
try { try {
if (value.isNullOrEmpty()) { if (value != null && Patterns.WEB_URL.matcher(value).matches() || URLUtil.isValidUrl(value)) {
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: WriterException) {
e.printStackTrace() e.printStackTrace()
return false return false
} }
@@ -244,7 +282,7 @@ object Utils {
} }
fun startVServiceFromToggle(context: Context): Boolean { fun startVServiceFromToggle(context: Context): Boolean {
if (MmkvManager.getSelectServer().isNullOrEmpty()) { if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
context.toast(R.string.app_tile_first_use) context.toast(R.string.app_tile_first_use)
return false return false
} }
@@ -279,7 +317,7 @@ object Utils {
fun urlDecode(url: String): String { fun urlDecode(url: String): String {
return try { return try {
URLDecoder.decode(url, Charsets.UTF_8.toString()) URLDecoder.decode(URLDecoder.decode(url), "utf-8")
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
url url
@@ -288,7 +326,7 @@ object Utils {
fun urlEncode(url: String): String { fun urlEncode(url: String): String {
return try { return try {
URLEncoder.encode(url, Charsets.UTF_8.toString()) URLEncoder.encode(url, "UTF-8")
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
url url
@@ -299,10 +337,7 @@ 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()
} }
@@ -313,23 +348,10 @@ object Utils {
if (context == null) if (context == null)
return "" return ""
val extDir = context.getExternalFilesDir(AppConfig.DIR_ASSETS) val extDir = context.getExternalFilesDir(AppConfig.DIR_ASSETS)
?: return context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath ?: return context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
return extDir.absolutePath return extDir.absolutePath
} }
fun backupPath(context: Context?): String {
if (context == null)
return ""
val extDir = context.getExternalFilesDir(AppConfig.DIR_BACKUPS)
?: return context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
return extDir.absolutePath
}
fun getDeviceIdForXUDPBaseKey(): String {
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
}
fun getUrlContext(url: String, timeout: Int): String { fun getUrlContext(url: String, timeout: Int): String {
var result: String var result: String
var conn: HttpURLConnection? = null var conn: HttpURLConnection? = null
@@ -352,27 +374,14 @@ object Utils {
} }
@Throws(IOException::class) @Throws(IOException::class)
fun getUrlContentWithCustomUserAgent(urlStr: String?, timeout: Int = 30000, httpPort: Int = 0): String { fun getUrlContentWithCustomUserAgent(urlStr: String?): String {
val url = URL(urlStr) val url = URL(urlStr)
val conn = if (httpPort == 0) { val conn = url.openConnection()
url.openConnection()
} else {
url.openConnection(
Proxy(
Proxy.Type.HTTP,
InetSocketAddress(LOOPBACK, 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( conn.setRequestProperty("Authorization",
"Authorization", "Basic ${encode(urlDecode(it))}")
"Basic ${encode(urlDecode(it))}"
)
} }
conn.useCaches = false conn.useCaches = false
return conn.inputStream.use { return conn.inputStream.use {
@@ -381,44 +390,27 @@ object Utils {
} }
fun getDarkModeStatus(context: Context): Boolean { fun getDarkModeStatus(context: Context): Boolean {
return context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK != UI_MODE_NIGHT_NO val mode = context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK
return mode == UI_MODE_NIGHT_YES
} }
fun getIpv6Address(address: String): String {
fun setNightMode(context: Context) { return if (isIpv6Address(address)) {
when (settingsStorage?.decodeString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
}
fun getIpv6Address(address: String?): String {
if (address == null) {
return ""
}
return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) {
String.format("[%s]", address) String.format("[%s]", address)
} else { } else {
address address
} }
} }
fun getLocale(): Locale { fun getLocale(context: Context): Locale =
val lang = settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: "auto" when (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")
"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]
@@ -428,42 +420,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? {
return str?.replace(" ", "")
}
fun idnToASCII(str: String): String {
val url = URL(str)
return URL(url.protocol, IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED), url.port, url.file)
.toExternalForm()
}
fun isTv(context: Context): Boolean =
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
}
}
fun findFreePort(ports: List<Int>): Int {
for (port in ports) {
try {
return ServerSocket(port).use { it.localPort }
} catch (ex: IOException) {
continue // try next port
}
}
// if the program gets here, no port in the range was found
throw IOException("no free port found")
} }
} }
@@ -3,83 +3,70 @@ package com.v2ray.ang.util
import android.content.Context import android.content.Context
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import com.google.gson.*
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.ANG_PACKAGE
import com.v2ray.ang.AppConfig.GEOIP_CN
import com.v2ray.ang.AppConfig.GEOSITE_CN
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM
import com.v2ray.ang.AppConfig.TAG_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.TAG_FRAGMENT
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
import com.v2ray.ang.dto.ConfigResult
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ERoutingMode
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK
import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
import com.v2ray.ang.util.MmkvManager.settingsStorage
object V2rayConfigUtil { object V2rayConfigUtil {
private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
fun getV2rayConfig(context: Context, guid: String): ConfigResult { data class Result(var status: Boolean, var content: String)
/**
* 生成v2ray的客户端配置文件
*/
fun getV2rayConfig(context: Context, guid: String): Result {
try { try {
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false) val config = MmkvManager.decodeServerConfig(guid) ?: return Result(false, "")
if (config.configType == EConfigType.CUSTOM) { if (config.configType == EConfigType.CUSTOM) {
val raw = MmkvManager.decodeServerRaw(guid) val raw = serverRawStorage?.decodeString(guid)
val customConfig = if (raw.isNullOrBlank()) { val customConfig = if (raw.isNullOrBlank()) {
config.fullConfig?.toPrettyPrinting() ?: return ConfigResult(false) config.fullConfig?.toPrettyPrinting() ?: return Result(false, "")
} else { } else {
raw raw
} }
val domainPort = config.getProxyOutbound()?.getServerAddressAndPort() Log.d(ANG_PACKAGE, customConfig)
return ConfigResult(true, guid, customConfig, domainPort) return Result(true, customConfig)
} }
val outbound = config.getProxyOutbound() ?: return Result(false, "")
val result = getV2rayNonCustomConfig(context, config) val result = getV2rayNonCustomConfig(context, outbound)
//Log.d(ANG_PACKAGE, result.content) Log.d(ANG_PACKAGE, result.content)
result.guid = guid
return result return result
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return ConfigResult(false) return Result(false, "")
} }
} }
private fun getV2rayNonCustomConfig(context: Context, config: ServerConfig): ConfigResult { /**
val result = ConfigResult(false) * 生成v2ray的客户端配置文件
*/
val outbound = config.getProxyOutbound() ?: return result private fun getV2rayNonCustomConfig(context: Context, outbound: V2rayConfig.OutboundBean): Result {
val address = outbound.getServerAddress() ?: return result val result = Result(false, "")
if (!Utils.isIpAddress(address)) {
if (!Utils.isValidUrl(address)) {
Log.d(ANG_PACKAGE, "$address is an invalid ip or domain")
return result
}
}
//取得默认配置 //取得默认配置
val assets = Utils.readTextFromAssets(context, "v2ray_config.json") val assets = Utils.readTextFromAssets(context, "v2ray_config.json")
if (TextUtils.isEmpty(assets)) { if (TextUtils.isEmpty(assets)) {
return result return result
} }
val v2rayConfig = JsonUtil.fromJson(assets, V2rayConfig::class.java) ?: return result
v2rayConfig.log.loglevel = settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL) ?: "warning" //转成Json
v2rayConfig.remarks = config.remarks val v2rayConfig = Gson().fromJson(assets, V2rayConfig::class.java) ?: return result
v2rayConfig.log.loglevel = settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL)
?: "warning"
inbounds(v2rayConfig) inbounds(v2rayConfig)
val isPlugin = outbound.protocol.equals(EConfigType.HYSTERIA2.name, true) httpRequestObject(outbound)
val retOut = outbounds(v2rayConfig, outbound, isPlugin)
val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin) v2rayConfig.outbounds[0] = outbound
routing(v2rayConfig) routing(v2rayConfig)
@@ -94,33 +81,31 @@ object V2rayConfigUtil {
v2rayConfig.stats = null v2rayConfig.stats = null
v2rayConfig.policy = null v2rayConfig.policy = null
} }
result.status = true result.status = true
result.content = v2rayConfig.toPrettyPrinting() result.content = v2rayConfig.toPrettyPrinting()
result.domainPort = if (retMore.first) retMore.second else retOut.second
return result return result
} }
/**
*
*/
private fun inbounds(v2rayConfig: V2rayConfig): Boolean { private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
try { try {
val socksPort = SettingsManager.getSocksPort() val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
val httpPort = SettingsManager.getHttpPort() val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
v2rayConfig.inbounds.forEach { curInbound -> v2rayConfig.inbounds.forEach { curInbound ->
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) { if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) {
//bind all inbounds to localhost if the user requests //bind all inbounds to localhost if the user requests
curInbound.listen = LOOPBACK curInbound.listen = "127.0.0.1"
} }
} }
v2rayConfig.inbounds[0].port = socksPort v2rayConfig.inbounds[0].port = socksPort
val fakedns = settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) val fakedns = settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED)
?: false ?: false
val sniffAllTlsAndHttp = val sniffAllTlsAndHttp = 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()
} }
@@ -143,58 +128,60 @@ object V2rayConfigUtil {
return true return true
} }
private fun outbounds(v2rayConfig: V2rayConfig, outbound: V2rayConfig.OutboundBean, isPlugin: Boolean): Pair<Boolean, String> {
if (isPlugin) {
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
val outboundNew = V2rayConfig.OutboundBean(
mux = null,
protocol = EConfigType.SOCKS.name.lowercase(),
settings = V2rayConfig.OutboundBean.OutSettingsBean(
servers = listOf(
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean(
address = LOOPBACK,
port = socksPort
)
)
)
)
if (v2rayConfig.outbounds.isNotEmpty()) {
v2rayConfig.outbounds[0] = outboundNew
} else {
v2rayConfig.outbounds.add(outboundNew)
}
return Pair(true, outboundNew.getServerAddressAndPort())
}
val ret = updateOutboundWithGlobalSettings(outbound)
if (!ret) return Pair(false, "")
if (v2rayConfig.outbounds.isNotEmpty()) {
v2rayConfig.outbounds[0] = outbound
} else {
v2rayConfig.outbounds.add(outbound)
}
updateOutboundFragment(v2rayConfig)
return Pair(true, outbound.getServerAddressAndPort())
}
private fun fakedns(v2rayConfig: V2rayConfig) { private fun fakedns(v2rayConfig: V2rayConfig) {
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true if (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 == "freedom" }.forEach {
it.settings?.domainStrategy = "UseIP"
}
} }
} }
/**
* routing
*/
private fun routing(v2rayConfig: V2rayConfig): Boolean { private fun routing(v2rayConfig: V2rayConfig): Boolean {
try { try {
routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
?: "", AppConfig.TAG_AGENT, v2rayConfig)
routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
?: "", AppConfig.TAG_DIRECT, v2rayConfig)
routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
?: "", AppConfig.TAG_BLOCKED, v2rayConfig)
v2rayConfig.routing.domainStrategy = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "IPIfNonMatch" v2rayConfig.routing.domainStrategy = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
?: "IPIfNonMatch"
v2rayConfig.routing.domainMatcher = "mph"
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value
val rulesetItems = MmkvManager.decodeRoutingRulesets() // Hardcode googleapis.cn
rulesetItems?.forEach { key -> val googleapisRoute = V2rayConfig.RoutingBean.RulesBean(
routingUserRule(key, v2rayConfig) type = "field",
outboundTag = AppConfig.TAG_AGENT,
domain = arrayListOf("domain:googleapis.cn")
)
when (routingMode) {
ERoutingMode.BYPASS_LAN.value -> {
routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig)
}
ERoutingMode.BYPASS_MAINLAND.value -> {
routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig)
v2rayConfig.routing.rules.add(0, googleapisRoute)
}
ERoutingMode.BYPASS_LAN_MAINLAND.value -> {
routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig)
routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig)
v2rayConfig.routing.rules.add(0, googleapisRoute)
}
ERoutingMode.GLOBAL_DIRECT.value -> {
val globalDirect = V2rayConfig.RoutingBean.RulesBean(
type = "field",
outboundTag = AppConfig.TAG_DIRECT,
port = "0-65535"
)
v2rayConfig.routing.rules.add(globalDirect)
}
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -203,101 +190,136 @@ object V2rayConfigUtil {
return true return true
} }
private fun routingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) { private fun routingGeo(ipOrDomain: String, code: String, tag: String, v2rayConfig: V2rayConfig) {
try { try {
if (item == null || !item.enabled) { if (!TextUtils.isEmpty(code)) {
return //IP
if (ipOrDomain == "ip" || ipOrDomain == "") {
val rulesIP = V2rayConfig.RoutingBean.RulesBean()
rulesIP.type = "field"
rulesIP.outboundTag = tag
rulesIP.ip = ArrayList()
rulesIP.ip?.add("geoip:$code")
v2rayConfig.routing.rules.add(rulesIP)
}
if (ipOrDomain == "domain" || ipOrDomain == "") {
//Domain
val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
rulesDomain.type = "field"
rulesDomain.outboundTag = tag
rulesDomain.domain = ArrayList()
rulesDomain.domain?.add("geosite:$code")
v2rayConfig.routing.rules.add(rulesDomain)
}
} }
val rule = JsonUtil.fromJson(JsonUtil.toJson(item), RulesBean::class.java) ?: return
v2rayConfig.routing.rules.add(rule)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
private fun userRule2Domain(tag: String): ArrayList<String> { private fun routingUserRule(userRule: String, tag: String, v2rayConfig: V2rayConfig) {
val domain = ArrayList<String>() try {
if (!TextUtils.isEmpty(userRule)) {
//Domain
val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
rulesDomain.type = "field"
rulesDomain.outboundTag = tag
rulesDomain.domain = ArrayList()
val rulesetItems = MmkvManager.decodeRoutingRulesets() //IP
rulesetItems?.forEach { key -> val rulesIP = V2rayConfig.RoutingBean.RulesBean()
if (key != null && key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) { rulesIP.type = "field"
key.domain?.forEach { rulesIP.outboundTag = tag
if (it != GEOSITE_PRIVATE rulesIP.ip = ArrayList()
&& (it.startsWith("geosite:") || it.startsWith("domain:"))
) { userRule.split(",").map { it.trim() }.forEach {
domain.add(it) if (Utils.isIpAddress(it) || it.startsWith("geoip:")) {
rulesIP.ip?.add(it)
} else if (it.isNotEmpty())
// if (Utils.isValidUrl(it)
// || it.startsWith("geosite:")
// || it.startsWith("regexp:")
// || it.startsWith("domain:")
// || it.startsWith("full:"))
{
rulesDomain.domain?.add(it)
} }
} }
if (rulesDomain.domain?.size!! > 0) {
v2rayConfig.routing.rules.add(rulesDomain)
}
if (rulesIP.ip?.size!! > 0) {
v2rayConfig.routing.rules.add(rulesIP)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun userRule2Domian(userRule: String): ArrayList<String> {
val domain = ArrayList<String>()
userRule.split(",").map { it.trim() }.forEach {
if (it.startsWith("geosite:") || it.startsWith("domain:")) {
domain.add(it)
} }
} }
return domain return domain
} }
/**
* Custom Dns
*/
private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean { private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean {
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 = userRule2Domain(TAG_PROXY) val proxyDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
val directDomain = userRule2Domain(TAG_DIRECT) ?: "")
val directDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
?: "")
// 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(0,
0, V2rayConfig.DnsBean.ServersBean(address = "fakedns", domains = geositeCn.plus(proxyDomain).plus(directDomain)))
V2rayConfig.DnsBean.ServersBean(
address = "fakedns",
domains = geositeCn.plus(proxyDomain).plus(directDomain)
)
)
} }
// DNS inbound对象 // DNS inbound对象
val remoteDns = Utils.getRemoteDnsServers() val remoteDns = Utils.getRemoteDnsServers()
if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) { if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) {
val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean( val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean(
address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else AppConfig.DNS_PROXY, address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else "1.1.1.1",
port = 53, port = 53,
network = "tcp,udp" network = "tcp,udp")
)
val localDnsPort = Utils.parseInt( val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT),
AppConfig.PORT_LOCAL_DNS.toInt()
)
v2rayConfig.inbounds.add( v2rayConfig.inbounds.add(
V2rayConfig.InboundBean( V2rayConfig.InboundBean(
tag = "dns-in", tag = "dns-in",
port = localDnsPort, port = localDnsPort,
listen = LOOPBACK, listen = "127.0.0.1",
protocol = "dokodemo-door", protocol = "dokodemo-door",
settings = dnsInboundSettings, settings = dnsInboundSettings,
sniffing = null sniffing = null))
)
)
} }
// DNS outbound对象 // DNS outbound对象
if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) { if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) {
v2rayConfig.outbounds.add( v2rayConfig.outbounds.add(
V2rayConfig.OutboundBean( V2rayConfig.OutboundBean(
protocol = "dns", protocol = "dns",
tag = "dns-out", tag = "dns-out",
settings = null, settings = null,
streamSettings = null, streamSettings = null,
mux = null mux = null))
)
)
} }
// DNS routing tag // DNS routing tag
v2rayConfig.routing.rules.add( v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean(
0, V2rayConfig.RoutingBean.RulesBean( type = "field",
inboundTag = arrayListOf("dns-in"), inboundTag = arrayListOf("dns-in"),
outboundTag = "dns-out", outboundTag = "dns-out",
domain = null domain = null)
)
) )
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -308,83 +330,66 @@ object V2rayConfigUtil {
private fun dns(v2rayConfig: V2rayConfig): Boolean { private fun dns(v2rayConfig: V2rayConfig): Boolean {
try { try {
val hosts = mutableMapOf<String, Any>() val hosts = mutableMapOf<String, String>()
val servers = ArrayList<Any>() val servers = ArrayList<Any>()
//remote Dns
val remoteDns = Utils.getRemoteDnsServers() val remoteDns = Utils.getRemoteDnsServers()
val proxyDomain = userRule2Domain(TAG_PROXY) val proxyDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
?: "")
remoteDns.forEach { remoteDns.forEach {
servers.add(it) servers.add(it)
} }
if (proxyDomain.size > 0) { if (proxyDomain.size > 0) {
servers.add( servers.add(V2rayConfig.DnsBean.ServersBean(remoteDns.first(), 53, proxyDomain, null))
V2rayConfig.DnsBean.ServersBean(
remoteDns.first(),
53,
proxyDomain,
null
)
)
} }
// domestic DNS // domestic DNS
val domesticDns = Utils.getDomesticDnsServers() val directDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
val directDomain = userRule2Domain(TAG_DIRECT) ?: "")
val isCnRoutingMode = directDomain.contains(GEOSITE_CN) val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value
val geoipCn = arrayListOf(GEOIP_CN) if (directDomain.size > 0 || routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
if (directDomain.size > 0) { val domesticDns = Utils.getDomesticDnsServers()
servers.add( val geositeCn = arrayListOf("geosite:cn")
V2rayConfig.DnsBean.ServersBean( val geoipCn = arrayListOf("geoip:cn")
domesticDns.first(), if (directDomain.size > 0) {
53, servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, directDomain, geoipCn))
directDomain, }
if (isCnRoutingMode) geoipCn else null if (routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, geositeCn, geoipCn))
}
if (Utils.isPureIpAddress(domesticDns.first())) {
v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean(
type = "field",
outboundTag = AppConfig.TAG_DIRECT,
port = "53",
ip = arrayListOf(domesticDns.first()),
domain = null)
) )
) }
} }
if (Utils.isPureIpAddress(domesticDns.first())) { val blkDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
v2rayConfig.routing.rules.add( ?: "")
0, V2rayConfig.RoutingBean.RulesBean(
outboundTag = TAG_DIRECT,
port = "53",
ip = arrayListOf(domesticDns.first()),
domain = null
)
)
}
//block dns
val blkDomain = userRule2Domain(TAG_BLOCKED)
if (blkDomain.size > 0) { if (blkDomain.size > 0) {
hosts.putAll(blkDomain.map { it to LOOPBACK }) hosts.putAll(blkDomain.map { it to "127.0.0.1" })
} }
// 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,
hosts = hosts hosts = hosts)
)
// DNS routing // DNS routing
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( type = "field",
outboundTag = TAG_PROXY, outboundTag = AppConfig.TAG_AGENT,
port = "53", port = "53",
ip = arrayListOf(remoteDns.first()), ip = arrayListOf(remoteDns.first()),
domain = null domain = null)
)
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -394,58 +399,17 @@ object V2rayConfigUtil {
return true return true
} }
private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean { private fun httpRequestObject(outbound: V2rayConfig.OutboundBean): Boolean {
try { try {
var muxEnabled = settingsStorage?.decodeBool(AppConfig.PREF_MUX_ENABLED, false)
val protocol = outbound.protocol
if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.SOCKS.name, true)
|| protocol.equals(EConfigType.HTTP.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true)
|| protocol.equals(EConfigType.WIREGUARD.name, true)
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
) {
muxEnabled = false
} else if (protocol.equals(EConfigType.VLESS.name, true)
&& outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.isNotEmpty() == true
) {
muxEnabled = false
}
if (muxEnabled == true) {
outbound.mux?.enabled = true
outbound.mux?.concurrency =
settingsStorage?.decodeInt(AppConfig.PREF_MUX_CONCURRENCY) ?: 8
outbound.mux?.xudpConcurrency =
settingsStorage?.decodeInt(AppConfig.PREF_MUX_XUDP_CONCURRENCY) ?: 8
outbound.mux?.xudpProxyUDP443 =
settingsStorage?.decodeString(AppConfig.PREF_MUX_XUDP_QUIC) ?: "reject"
} else {
outbound.mux?.enabled = false
outbound.mux?.concurrency = -1
}
if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
var localTunAddr = if (outbound.settings?.address == null) {
listOf(WIREGUARD_LOCAL_ADDRESS_V4, WIREGUARD_LOCAL_ADDRESS_V6)
} else {
outbound.settings?.address as List<*>
}
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) != true) {
localTunAddr = listOf(localTunAddr.first())
}
outbound.settings?.address = localTunAddr
}
if (outbound.streamSettings?.network == DEFAULT_NETWORK if (outbound.streamSettings?.network == DEFAULT_NETWORK
&& outbound.streamSettings?.tcpSettings?.header?.type == HTTP && outbound.streamSettings?.tcpSettings?.header?.type == HTTP) {
) {
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
val requestString: String by lazy { val requestString: String by lazy {
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}""" """{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
} }
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson( outbound.streamSettings?.tcpSettings?.header?.request = Gson().fromJson(
requestString, requestString,
V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
) )
@@ -455,10 +419,9 @@ object V2rayConfigUtil {
} else { } else {
path path
} }
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host!!
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return false return false
@@ -466,132 +429,4 @@ object V2rayConfigUtil {
return true return true
} }
private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean { }
try {
if (settingsStorage?.decodeBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
return true
}
if (v2rayConfig.outbounds[0].streamSettings?.security != V2rayConfig.TLS
&& v2rayConfig.outbounds[0].streamSettings?.security != V2rayConfig.REALITY
) {
return true
}
val fragmentOutbound =
V2rayConfig.OutboundBean(
protocol = PROTOCOL_FREEDOM,
tag = TAG_FRAGMENT,
mux = null
)
var packets =
settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
if (v2rayConfig.outbounds[0].streamSettings?.security == V2rayConfig.REALITY
&& packets == "tlshello"
) {
packets = "1-3"
} else if (v2rayConfig.outbounds[0].streamSettings?.security == V2rayConfig.TLS
&& packets != "tlshello"
) {
packets = "tlshello"
}
fragmentOutbound.settings = V2rayConfig.OutboundBean.OutSettingsBean(
fragment = V2rayConfig.OutboundBean.OutSettingsBean.FragmentBean(
packets = packets,
length = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_LENGTH)
?: "50-100",
interval = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL)
?: "10-20"
),
noises = listOf(
V2rayConfig.OutboundBean.OutSettingsBean.NoiseBean(
type = "rand",
packet = "100-200",
delay = "10-20",
)
),
)
fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean(
sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
TcpNoDelay = true,
mark = 255
)
)
v2rayConfig.outbounds.add(fragmentOutbound)
//proxy chain
v2rayConfig.outbounds[0].streamSettings?.sockopt =
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
dialerProxy = TAG_FRAGMENT
)
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
private fun moreOutbounds(v2rayConfig: V2rayConfig, subscriptionId: String, isPlugin: Boolean): Pair<Boolean, String> {
val returnPair = Pair(false, "")
var domainPort: String = ""
if (isPlugin) {
return returnPair
}
//fragment proxy
if (settingsStorage?.decodeBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
return returnPair
}
if (subscriptionId.isNullOrEmpty()) {
return returnPair
}
try {
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return returnPair
//current proxy
val outbound = v2rayConfig.outbounds[0]
//Previous proxy
val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile)
if (prevNode != null) {
val prevOutbound = prevNode.getProxyOutbound()
if (prevOutbound != null) {
updateOutboundWithGlobalSettings(prevOutbound)
prevOutbound.tag = TAG_PROXY + "2"
v2rayConfig.outbounds.add(prevOutbound)
outbound.streamSettings?.sockopt =
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
dialerProxy = prevOutbound.tag
)
domainPort = prevOutbound.getServerAddressAndPort()
}
}
//Next proxy
val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile)
if (nextNode != null) {
val nextOutbound = nextNode.getProxyOutbound()
if (nextOutbound != null) {
updateOutboundWithGlobalSettings(nextOutbound)
nextOutbound.tag = TAG_PROXY
v2rayConfig.outbounds.add(0, nextOutbound)
outbound.tag = TAG_PROXY + "1"
nextOutbound.streamSettings?.sockopt =
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
dialerProxy = outbound.tag
)
}
}
} catch (e: Exception) {
e.printStackTrace()
return returnPair
}
if (domainPort.isNotEmpty()) {
return Pair(true, domainPort)
}
return returnPair
}
}
@@ -1,102 +0,0 @@
package com.v2ray.ang.util
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
object ZipUtil {
private const val BUFFER_SIZE = 4096
@Throws(IOException::class)
fun zipFromFolder(folderPath: String, outputZipFilePath: String): Boolean {
val buffer = ByteArray(BUFFER_SIZE)
try {
if (folderPath.isEmpty() || outputZipFilePath.isEmpty()) {
return false
}
val filesToCompress = ArrayList<String>()
val directory = File(folderPath)
if (directory.isDirectory) {
directory.listFiles()?.forEach {
if (it.isFile) {
filesToCompress.add(it.absolutePath)
}
}
}
if (filesToCompress.isEmpty()) {
return false
}
val zos = ZipOutputStream(FileOutputStream(outputZipFilePath))
filesToCompress.forEach { file ->
val ze = ZipEntry(File(file).name)
zos.putNextEntry(ze)
val inputStream = FileInputStream(file)
while (true) {
val len = inputStream.read(buffer)
if (len <= 0) break
zos.write(buffer, 0, len)
}
inputStream.close()
}
zos.closeEntry()
zos.close()
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
@Throws(IOException::class)
fun unzipToFolder(zipFile: File, destDirectory: String): Boolean {
File(destDirectory).run {
if (!exists()) {
mkdirs()
}
}
try {
ZipFile(zipFile).use { zip ->
zip.entries().asSequence().forEach { entry ->
zip.getInputStream(entry).use { input ->
val filePath = destDirectory + File.separator + entry.name
if (!entry.isDirectory) {
// if the entry is a file, extracts it
extractFile(input, filePath)
} else {
// if the entry is a directory, make the directory
val dir = File(filePath)
dir.mkdir()
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
@Throws(IOException::class)
private fun extractFile(inputStream: InputStream, destFilePath: String) {
val bos = BufferedOutputStream(FileOutputStream(destFilePath))
val bytesIn = ByteArray(BUFFER_SIZE)
var read: Int
while (inputStream.read(bytesIn).also { read = it } != -1) {
bos.write(bytesIn, 0, read)
}
bos.close()
}
}
@@ -1,104 +0,0 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.util.Utils
open class FmtBase {
fun toUri(address: String?, port: Int?, userInfo: String?, dicQuery: HashMap<String, String>?, remark: String): String {
val query = if (dicQuery != null)
("?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + Utils.urlEncode(it.second) }))
else ""
val url = String.format(
"%s@%s:%s",
Utils.urlEncode(userInfo ?: ""),
Utils.getIpv6Address(address),
port
)
return "${url}${query}#${Utils.urlEncode(remark)}"
}
fun getStdTransport(outbound: V2rayConfig.OutboundBean, streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean): HashMap<String, String> {
val dicQuery = HashMap<String, String>()
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
(streamSetting.tlsSettings
?: streamSetting.realitySettings)?.let { tlsSetting ->
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
dicQuery["sni"] = tlsSetting.serverName
}
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
dicQuery["alpn"] =
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
dicQuery["fp"] = tlsSetting.fingerprint.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
dicQuery["pbk"] = tlsSetting.publicKey.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
dicQuery["sid"] = tlsSetting.shortId.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
dicQuery["spx"] = tlsSetting.spiderX.orEmpty()
}
}
dicQuery["type"] =
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
outbound.getTransportSettingDetails()?.let { transportDetails ->
when (streamSetting.network) {
"tcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = transportDetails[1]
}
}
"kcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["seed"] = transportDetails[2]
}
}
"ws", "httpupgrade", "splithttp" -> {
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = transportDetails[1]
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = transportDetails[2]
}
}
"http", "h2" -> {
dicQuery["type"] = "http"
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = transportDetails[1]
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = transportDetails[2]
}
}
"quic" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
dicQuery["quicSecurity"] = transportDetails[1]
dicQuery["key"] = transportDetails[2]
}
"grpc" -> {
dicQuery["mode"] = transportDetails[0]
dicQuery["authority"] = transportDetails[1]
dicQuery["serviceName"] = transportDetails[2]
}
}
}
return dicQuery
}
}
@@ -1,102 +0,0 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.Hysteria2Bean
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import java.net.URI
object Hysteria2Fmt : FmtBase() {
fun parse(str: String): ServerConfig {
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val config = ServerConfig.create(EConfigType.HYSTERIA2)
val uri = URI(Utils.fixIllegalUrl(str))
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?.streamSettings?.populateTlsSettings(
V2rayConfig.TLS,
if ((queryParam["insecure"].orEmpty()) == "1") true else allowInsecure,
queryParam["sni"] ?: uri.idnHost,
null,
queryParam["alpn"],
null,
null,
null
)
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = uri.idnHost
server.port = uri.port
server.password = uri.userInfo
}
if (!queryParam["obfs-password"].isNullOrEmpty()) {
config.outboundBean?.settings?.obfsPassword = queryParam["obfs-password"]
}
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
val dicQuery = HashMap<String, String>()
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
streamSetting.tlsSettings?.let { tlsSetting ->
dicQuery["insecure"] = if (tlsSetting.allowInsecure) "1" else "0"
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 (!outbound.settings?.obfsPassword.isNullOrEmpty()) {
dicQuery["obfs"] = "salamander"
dicQuery["obfs-password"] = outbound.settings?.obfsPassword ?: ""
}
return toUri(outbound.getServerAddress(), outbound.getServerPort(), outbound.getPassword(), dicQuery, config.remarks)
}
fun toNativeConfig(config: ServerConfig, socksPort: Int): Hysteria2Bean? {
val outbound = config.getProxyOutbound() ?: return null
val tls = outbound.streamSettings?.tlsSettings
val obfs = if (outbound.settings?.obfsPassword.isNullOrEmpty()) null else
Hysteria2Bean.ObfsBean(
type = "salamander",
salamander = Hysteria2Bean.ObfsBean.SalamanderBean(
password = outbound.settings?.obfsPassword
)
)
val bean = Hysteria2Bean(
server = outbound.getServerAddressAndPort(),
auth = outbound.getPassword(),
obfs = obfs,
socks5 = Hysteria2Bean.Socks5Bean(
listen = "$LOOPBACK:${socksPort}",
),
http = Hysteria2Bean.Socks5Bean(
listen = "$LOOPBACK:${socksPort}",
),
tls = Hysteria2Bean.TlsBean(
sni = tls?.serverName ?: outbound.getServerAddress(),
insecure = tls?.allowInsecure
)
)
return bean
}
}
@@ -1,153 +0,0 @@
package com.v2ray.ang.util.fmt
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.Utils
import java.net.URI
object ShadowsocksFmt : FmtBase() {
fun parse(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 pw = Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
return toUri(outbound.getServerAddress(), outbound.getServerPort(), pw, null, config.remarks)
}
private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {
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
}
}
}
@@ -1,64 +0,0 @@
package com.v2ray.ang.util.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.util.Utils
object SocksFmt : FmtBase() {
fun parse(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 pw =
if (outbound.settings?.servers?.get(0)?.users?.get(0)?.user != null)
"${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}"
else
":"
return toUri(outbound.getServerAddress(), outbound.getServerPort(), pw, null, config.remarks)
}
}
@@ -1,90 +0,0 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
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.settingsStorage
import com.v2ray.ang.util.Utils
import java.net.URI
object TrojanFmt : FmtBase() {
fun parse(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 dicQuery = getStdTransport(outbound, streamSetting)
config.outboundBean?.settings?.servers?.get(0)?.flow?.let {
if (!TextUtils.isEmpty(it)) {
dicQuery["flow"] = it
}
}
return toUri(outbound.getServerAddress(), outbound.getServerPort(), outbound.getPassword(), dicQuery, config.remarks)
}
}
@@ -1,80 +0,0 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
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.settingsStorage
import com.v2ray.ang.util.Utils
import java.net.URI
object VlessFmt : FmtBase() {
fun parse(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 dicQuery = getStdTransport(outbound, streamSetting)
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()
return toUri(outbound.getServerAddress(), outbound.getServerPort(), outbound.getPassword(), dicQuery, config.remarks)
}
}
@@ -1,155 +0,0 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.VmessQRCode
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import java.net.URI
object VmessFmt : FmtBase() {
fun parse(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 = JsonUtil.fromJson(result, VmessQRCode::class.java)
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
if (TextUtils.isEmpty(vmessQRCode.add)
|| TextUtils.isEmpty(vmessQRCode.port)
|| TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net)
) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
return null
}
config.remarks = vmessQRCode.ps
config.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 = JsonUtil.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
}
}
@@ -1,92 +0,0 @@
package com.v2ray.ang.util.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.util.Utils
import java.net.URI
object WireguardFmt : FmtBase() {
fun parse(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 dicQuery = HashMap<String, String>()
dicQuery["publickey"] = outbound.settings?.peers?.get(0)?.publicKey.toString()
if (outbound.settings?.reserved != null) {
dicQuery["reserved"] = Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString(",")).toString()
}
dicQuery["address"] = Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString(",")).toString()
if (outbound.settings?.mtu != null) {
dicQuery["mtu"] = outbound.settings?.mtu.toString()
}
return toUri(outbound.getServerAddress(), outbound.getServerPort(), outbound.getPassword(), dicQuery, config.remarks)
}
}
@@ -1,69 +1,46 @@
package com.v2ray.ang.viewmodel package com.v2ray.ang.viewmodel
import android.app.Application import android.app.Application
import android.content.BroadcastReceiver import android.content.*
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.AssetManager
import android.os.Build
import android.util.Log import 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.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.dto.EConfigType import com.v2ray.ang.databinding.DialogConfigFilterBinding
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.*
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.serializable
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager import com.v2ray.ang.util.*
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub import com.v2ray.ang.util.MmkvManager.KEY_ANG_CONFIGS
import com.v2ray.ang.util.JsonUtil import kotlinx.coroutines.*
import com.v2ray.ang.util.MessageUtil import java.util.*
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.util.Collections
class MainViewModel(application: Application) : AndroidViewModel(application) { class MainViewModel(application: Application) : AndroidViewModel(application) {
private var serverList = MmkvManager.decodeServerList() private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
var subscriptionId: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty() private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
//var keywordFilter: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")?:"" var serverList = MmkvManager.decodeServerList()
var keywordFilter = "" var subscriptionId: String = ""
var keywordFilter: String = ""
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() {
isRunning.value = false isRunning.value = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { getApplication<AngApplication>().registerReceiver(mMsgReceiver, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
getApplication<AngApplication>().registerReceiver(
mMsgReceiver,
IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY),
Context.RECEIVER_EXPORTED
)
} else {
getApplication<AngApplication>().registerReceiver(
mMsgReceiver,
IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
)
}
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "") MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "")
} }
@@ -84,116 +61,58 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun removeServer(guid: String) { fun removeServer(guid: String) {
serverList.remove(guid) serverList.remove(guid)
MmkvManager.removeServer(guid) MmkvManager.removeServer(guid)
val index = getPosition(guid) serversCache.removeAt(getPosition(guid))
if (index >= 0) {
serversCache.removeAt(index)
}
} }
fun appendCustomConfigServer(server: String): Boolean { fun appendCustomConfigServer(server: String) {
if (server.contains("inbounds") val config = ServerConfig.create(EConfigType.CUSTOM)
&& server.contains("outbounds") config.remarks = System.currentTimeMillis().toString()
&& server.contains("routing") config.subscriptionId = subscriptionId
) { config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
try { val key = MmkvManager.encodeServerConfig("", config)
val config = ServerConfig.create(EConfigType.CUSTOM) serverRawStorage?.encode(key, server)
config.subscriptionId = subscriptionId serverList.add(key)
config.fullConfig = JsonUtil.fromJson(server, V2rayConfig::class.java) serversCache.add(ServersCache(key,config))
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
serverList.add(0, key)
val profile = ProfileItem(
configType = config.configType,
subscriptionId = config.subscriptionId,
remarks = config.remarks,
server = config.getProxyOutbound()?.getServerAddress(),
serverPort = config.getProxyOutbound()?.getServerPort(),
)
serversCache.add(0, ServersCache(key, profile))
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)
MmkvManager.encodeServerList(serverList) 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) val config = MmkvManager.decodeServerConfig(guid) ?: continue
if (profile == null) { if (subscriptionId.isNotEmpty() && subscriptionId != config.subscriptionId) {
val config = MmkvManager.decodeServerConfig(guid) ?: continue
profile = ProfileItem(
configType = config.configType,
subscriptionId = config.subscriptionId,
remarks = config.remarks,
server = config.getProxyOutbound()?.getServerAddress(),
serverPort = config.getProxyOutbound()?.getServerPort(),
)
MmkvManager.encodeServerConfig(guid, config)
}
if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) {
continue continue
} }
if (keywordFilter.isEmpty() || profile.remarks.contains(keywordFilter)) { if (keywordFilter.isEmpty() || config.remarks.contains(keywordFilter)) {
serversCache.add(ServersCache(guid, profile)) serversCache.add(ServersCache(guid, config))
} }
} }
} }
fun updateConfigViaSubAll(): Int {
if (subscriptionId.isNullOrEmpty()) {
return AngConfigManager.updateConfigViaSubAll()
} else {
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return 0
return updateConfigViaSub(Pair(subscriptionId, subItem))
}
}
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(serversCache.map { it.guid }.toList()) MmkvManager.clearAllTestDelayResults()
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.profile.let { outbound -> item.config.getProxyOutbound()?.let { outbound ->
val serverAddress = outbound.server val serverAddress = outbound.getServerAddress()
val serverPort = outbound.serverPort val serverPort = outbound.getServerPort()
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)
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult) MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
updateListAction.value = getPosition(item.guid) updateListAction.value = getPosition(item.guid)
} }
} }
} }
@@ -203,15 +122,16 @@ 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(serversCache.map { it.guid }.toList()) MmkvManager.clearAllTestDelayResults()
updateListAction.value = -1 // update all updateListAction.value = -1 // update all
val serversCopy = serversCache.toList() // Create a copy of the list
getApplication<AngApplication>().toast(R.string.connection_test_testing) getApplication<AngApplication>().toast(R.string.connection_test_testing)
viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread
for (item in serversCopy) { for (item in serversCache) {
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, item.guid) val config = V2rayConfigUtil.getV2rayConfig(getApplication(), item.guid)
if (config.status) {
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, Pair(item.guid, config.content))
}
} }
} }
} }
@@ -220,33 +140,57 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "") MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "")
} }
fun subscriptionIdChanged(id: String) { fun filterConfig(context :Context) {
if (subscriptionId != id) {
subscriptionId = id
MmkvManager.settingsStorage.encode(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId)
reloadServerList()
}
}
fun getSubscriptions(context: Context): Pair<MutableList<String>?, MutableList<String>?> {
val subscriptions = MmkvManager.decodeSubscriptions() val subscriptions = MmkvManager.decodeSubscriptions()
if (subscriptionId.isNotEmpty() val listId = subscriptions.map { it.first }.toList().toMutableList()
&& !subscriptions.map { it.first }.contains(subscriptionId) val listRemarks = subscriptions.map { it.second.remarks }.toList().toMutableList()
) { listRemarks += context.getString(R.string.filter_config_all)
subscriptionIdChanged("") val checkedItem = if (subscriptionId.isNotEmpty()) {
listId.indexOf(subscriptionId)
} else {
listRemarks.count() - 1
} }
if (subscriptions.isEmpty()) {
return null to null
}
val listId = subscriptions.map { it.first }.toMutableList()
listId.add(0, "")
val listRemarks = subscriptions.map { it.second.remarks }.toMutableList()
listRemarks.add(0, context.getString(R.string.filter_config_all))
return listId to listRemarks 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()
reloadServerList()
dialogInterface?.dismiss()
} catch (e: Exception) {
e.printStackTrace()
}
}
builder.show()
// AlertDialog.Builder(context)
// .setSingleChoiceItems(listRemarks.toTypedArray(), checkedItem) { dialog, i ->
// try {
// subscriptionId = if (listRemarks.count() - 1 == i) {
// ""
// } else {
// subscriptions[i].first
// }
// reloadServerList()
// dialog.dismiss()
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }.show()
} }
fun getPosition(guid: String): Int { fun getPosition(guid: String) : Int {
serversCache.forEachIndexed { index, it -> serversCache.forEachIndexed { index, it ->
if (it.guid == guid) if (it.guid == guid)
return index return index
@@ -254,144 +198,35 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return -1 return -1
} }
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>()
serversCacheCopy.forEachIndexed { index, it ->
val outbound = it.second.getProxyOutbound()
serversCacheCopy.forEachIndexed { index2, it2 ->
if (index2 > index) {
val outbound2 = it2.second.getProxyOutbound()
if (outbound == outbound2 && !deleteServer.contains(it2.first)) {
deleteServer.add(it2.first)
}
}
}
}
for (it in deleteServer) {
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() {
data class ServerDelay(var guid: String, var testDelayMillis: Long)
val serverDelays = mutableListOf<ServerDelay>()
val serverList = MmkvManager.decodeServerList()
serverList.forEach { key ->
val delay = MmkvManager.decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L
serverDelays.add(ServerDelay(key, if (delay <= 0L) 999999 else delay))
}
serverDelays.sortBy { it.testDelayMillis }
serverDelays.forEach {
serverList.remove(it.guid)
serverList.add(it.guid)
}
MmkvManager.encodeServerList(serverList)
}
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()
}
private val mMsgReceiver = object : BroadcastReceiver() { private val mMsgReceiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context?, intent: Intent?) { override fun onReceive(ctx: Context?, intent: Intent?) {
when (intent?.getIntExtra("key", 0)) { when (intent?.getIntExtra("key", 0)) {
AppConfig.MSG_STATE_RUNNING -> { AppConfig.MSG_STATE_RUNNING -> {
isRunning.value = true isRunning.value = true
} }
AppConfig.MSG_STATE_NOT_RUNNING -> { AppConfig.MSG_STATE_NOT_RUNNING -> {
isRunning.value = false isRunning.value = false
} }
AppConfig.MSG_STATE_START_SUCCESS -> { AppConfig.MSG_STATE_START_SUCCESS -> {
getApplication<AngApplication>().toast(R.string.toast_services_success) getApplication<AngApplication>().toast(R.string.toast_services_success)
isRunning.value = true isRunning.value = true
} }
AppConfig.MSG_STATE_START_FAILURE -> { AppConfig.MSG_STATE_START_FAILURE -> {
getApplication<AngApplication>().toast(R.string.toast_services_failure) getApplication<AngApplication>().toast(R.string.toast_services_failure)
isRunning.value = false isRunning.value = false
} }
AppConfig.MSG_STATE_STOP_SUCCESS -> { AppConfig.MSG_STATE_STOP_SUCCESS -> {
isRunning.value = false isRunning.value = false
} }
AppConfig.MSG_MEASURE_DELAY_SUCCESS -> { AppConfig.MSG_MEASURE_DELAY_SUCCESS -> {
updateTestResultAction.value = intent.getStringExtra("content") updateTestResultAction.value = intent.getStringExtra("content")
} }
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> { AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
val resultPair = intent.serializable<Pair<String, Long>>("content") ?: return val resultPair = 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)
} }
} }
} }
} }
} }

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