Compare commits

...

445 Commits

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

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml
2024-05-07 09:42:15 +08:00
2dust
a1d68fcde3 Added attempt to update subscription via proxy
https://github.com/2dust/v2rayNG/issues/2760
2024-05-05 17:08:05 +08:00
2dust
e053db3dff Add routeOnly 2024-05-02 10:51:54 +08:00
solokot
f624bd651e Update Russian translation (#3070) 2024-05-01 19:37:03 +08:00
2dust
84964c7f91 Add attributes to grpc
https://github.com/2dust/v2rayNG/issues/3041
2024-05-01 14:01:34 +08:00
2dust
96f56b468e Adjust DNS hardcoded geoip:cn issue
https://github.com/2dust/v2rayNG/issues/3061
2024-05-01 14:00:35 +08:00
2dust
bdc3212f38 Rule start with ext and contains geoip added to the ips
https://github.com/2dust/v2rayNG/issues/3060
2024-05-01 10:43:47 +08:00
2dust
508ddf6df2 Share configuration 2024-04-28 19:47:54 +08:00
2dust
17af24d179 up 1.8.22 2024-04-26 13:45:59 +08:00
2dust
c260e447ea Adjust UI 2024-04-25 17:52:34 +08:00
2dust
0eb40ae993 Adjust UI 2024-04-25 10:34:15 +08:00
ibrahem Qasim
18f3e39346 Update Arabic translation (#3042)
* Create android.yml

* Create gradle-publish.yml

* Update strings.xml

* Delete .github/workflows/android.yml

* Delete .github/workflows/gradle-publish.yml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml
2024-04-25 08:17:58 +08:00
2dust
311af02726 up 1.8.21 2024-04-23 17:32:20 +08:00
2dust
b799969de8 Adjust UI 2024-04-23 13:45:50 +08:00
2dust
93541883bb Adjust UI 2024-04-22 10:35:16 +08:00
2dust
33430bff8d Adjust UI 2024-04-21 16:31:34 +08:00
2dust
3bc2081540 Adjust UI 2024-04-21 12:05:42 +08:00
2dust
a3b1feabff Bug fix
https://github.com/2dust/v2rayNG/issues/3024
2024-04-20 15:38:55 +08:00
2dust
7af8d3843e Merge pull request #3025 from solokot/master
Update Russian translation
2024-04-20 15:11:02 +08:00
solokot
2235805800 Update Russian translation 2024-04-19 18:40:09 +03:00
2dust
364b521ec3 up 1.8.20 2024-04-19 21:07:38 +08:00
2dust
3cdaf4a8ee Bug fix
https://github.com/2dust/v2rayNG/issues/3010
2024-04-19 14:36:48 +08:00
2dust
b2fc6dcdd0 Add switching theme dark or light 2024-04-19 13:41:52 +08:00
2dust
70e9320463 Merge pull request #3020 from FranzKafkaYu/optimize-ui-logic
style:optimize UI logic
2024-04-18 10:42:51 +08:00
FranzKafkayu
304c7ed068 style:optimize UI logic
1.add button for cancelling delete server config
2.toast when delete current using config
2024-04-17 18:37:17 +08:00
2dust
ddb908f937 Bug fix 2024-04-17 16:39:53 +08:00
2dust
accd17afd4 Bug fix 2024-04-17 15:49:07 +08:00
2dust
9f598b77b4 Merge pull request #3016 from solokot/master
Update Russian translation
2024-04-17 14:11:28 +08:00
solokot
d82fa974b1 Update Russian translation 2024-04-16 17:13:22 +03:00
2dust
fcbd4a0d48 Optimized storage of settings for SharedPreference 2024-04-16 20:23:58 +08:00
2dust
5cd2b8845e Adjust about style 2024-04-16 10:39:56 +08:00
2dust
723ab70170 Remove function migrateLegacy() 2024-04-16 10:29:07 +08:00
2dust
a26bf3eeda Add backup and restore configuration functionality 2024-04-15 17:30:00 +08:00
2dust
c33b6463c6 Merge pull request #3005 from solokot/master
Update Russian translation
2024-04-13 20:23:06 +08:00
solokot
df995a3ab2 Update Russian translation 2024-04-13 10:39:33 +03:00
2dust
817f844212 Add about activity 2024-04-13 13:12:35 +08:00
2dust
fa8113b8d7 Merge pull request #3002 from NetworkKeeper/patch-1
Ability to override built in `geosite.dat` and `geoip.dat`
2024-04-12 07:47:44 +08:00
2dust
99b95d8369 Merge pull request #3001 from NetworkKeeper/patch-2
Retry downloading geo assets without proxy
2024-04-12 07:47:19 +08:00
2dust
fc2e4ff210 Optimize routing 2024-04-12 07:45:26 +08:00
NetworkKeeper
1063bf71d6 Retry downloading geo assets without proxy 2024-04-11 19:37:20 +03:00
NetworkKeeper
f2af5c45e9 Ability to override built in geosite.dat and geoip.dat 2024-04-11 19:30:32 +03:00
2dust
b132b0d2f0 Remove the type of routing rule 2024-04-11 11:00:46 +08:00
2dust
7869f99fc8 Adjust routing default rules 2024-04-11 09:21:57 +08:00
2dust
122f2eb400 Merge pull request #2995 from oXIIIo/master
fix(deps): Update Go version to 1.22.2 for Xray compatibility (1.8.10)
2024-04-11 07:47:36 +08:00
XIII
3c0f6eeb21 fix(deps): Update Go version to 1.22.2 for Xray compatibility (1.8.10) 2024-04-10 17:36:39 +03:30
2dust
19a109355b Merge pull request #2986 from kimsuelim/improve_build
Remove -XX:MaxPermSize option
2024-04-08 17:12:15 +08:00
Surim Kim
703965a0dd Remove -XX:MaxPermSize option.
Android Gradle requires JDK 17, and this option was removed between Java 11 and Java 17.
2024-04-08 10:05:28 +09:00
2dust
66f92c6c60 Bug fix
https://github.com/2dust/v2rayNG/issues/2972
2024-04-03 10:19:03 +08:00
2dust
a1cdf6b7a5 Merge pull request #2966 from Malus-risus/master
Update dependency
2024-04-03 10:02:45 +08:00
Το μοχθηρό ^_^
554c7b5687 Update build.gradle.kts 2024-04-02 09:34:43 +08:00
Το μοχθηρό ^_^
2d987313a7 Delete renovate.json 2024-04-01 14:02:42 +08:00
Το μοχθηρό ^_^
9eebe32bdf Merge pull request #31 from Malus-risus/renovate/com.android.library-8.x
Update plugin com.android.library to v8.3.1
2024-04-01 13:24:07 +08:00
renovate[bot]
8e9da0ad6f Update plugin com.android.library to v8.3.1 2024-04-01 05:15:22 +00:00
Το μοχθηρό ^_^
2f20dea611 Merge pull request #30 from Malus-risus/renovate/com.android.application-8.x
Update plugin com.android.application to v8.3.1
2024-04-01 13:14:51 +08:00
renovate[bot]
167baf64a9 Update plugin com.android.application to v8.3.1 2024-04-01 04:47:28 +00:00
Το μοχθηρό ^_^
bd7a214f7f Merge pull request #29 from Malus-risus/renovate/gradle-8.x
Update dependency gradle to v8.7
2024-04-01 12:17:52 +08:00
Το μοχθηρό ^_^
f16d2d9a74 Merge pull request #28 from Malus-risus/renovate/com.tencent-mmkv-static-1.x
Update dependency com.tencent:mmkv-static to v1.3.4
2024-04-01 12:17:42 +08:00
renovate[bot]
e984d2c274 Update dependency gradle to v8.7 2024-04-01 04:09:58 +00:00
renovate[bot]
8720d087ea Update dependency com.tencent:mmkv-static to v1.3.4 2024-04-01 04:09:24 +00:00
Το μοχθηρό ^_^
a1e19b9fcd Create renovate.json 2024-04-01 12:08:53 +08:00
2dust
6b9728dc84 Update 1.8.19 2024-03-30 17:25:08 +08:00
2dust
1ce9b7c0c8 Merge pull request #2956 from xfree-man/master
Add observatory to V2rayConfig, to enable load balancing features in a custom config
2024-03-27 07:53:57 +08:00
xfree-man
ff75d3fdc2 Add observatory to V2rayConfig 2024-03-26 13:31:04 +04:00
2dust
8154812570 Merge pull request #2948 from Amir-yazdanmanesh/master
Fix deprecated codes
2024-03-22 09:58:44 +08:00
amir
1dcd2478fc Expression should use clarifying parentheses 2024-03-20 12:37:53 +03:30
amir
0515806e92 Remove deprecated codes in MainActivity.kt 2024-03-20 12:37:32 +03:30
2dust
f286506ba4 Bug fix 2024-03-16 17:11:38 +08:00
2dust
48be736275 Merge pull request #2933 from Utaea/pr
Add support to transport for shadowsocks
2024-03-16 16:58:20 +08:00
2dust
af0faeab16 Merge pull request #2928 from yuhan6665/authority
Add authority for gRPC
2024-03-16 16:31:12 +08:00
2dust
fb6edee842 Merge branch 'master' into authority 2024-03-16 16:30:28 +08:00
2dust
1209c4b92a Merge pull request #2921 from solokot/master
Update & improve Russian translation
2024-03-16 16:26:31 +08:00
Utaea
e4e668b492 Add support to transport for shadowsocks 2024-03-15 03:51:49 +00:00
yuhan6665
92d3136a23 Add authority for gRPC 2024-03-13 12:57:08 -04:00
solokot
f6e74ddb45 Update & improve Russian translation 2024-03-12 10:22:16 +03:00
2dust
c34f332771 Update 1.8.18 2024-03-12 10:49:52 +08:00
2dust
eb1d71bda6 Add httpupgrade 2024-03-12 10:49:32 +08:00
2dust
3cdd5822cb Merge pull request #2915 from Malus-risus/master
Update dependency
2024-03-10 10:25:07 +08:00
Το μοχθηρό ^_^
8f924b5ce1 Delete renovate.json 2024-03-09 17:33:08 +08:00
Το μοχθηρό ^_^
2ae645dce5 Merge pull request #24 from Malus-risus/renovate/com.android.library-8.x
Update plugin com.android.library to v8.3.0
2024-03-09 17:20:13 +08:00
renovate[bot]
f848a7119f Update plugin com.android.library to v8.3.0 2024-03-09 09:12:10 +00:00
Το μοχθηρό ^_^
2fa41db32f Merge pull request #23 from Malus-risus/renovate/com.android.application-8.x
Update plugin com.android.application to v8.3.0
2024-03-09 17:11:24 +08:00
Το μοχθηρό ^_^
9af4516761 Merge pull request #22 from Malus-risus/renovate/kotlin-ksp
Update Kotlin, KSP to v1.9.23
2024-03-09 17:11:03 +08:00
renovate[bot]
189b07124d Update plugin com.android.application to v8.3.0 2024-03-09 08:19:13 +00:00
renovate[bot]
e6e5cdcdcd Update Kotlin, KSP to v1.9.23 2024-03-09 08:19:07 +00:00
Το μοχθηρό ^_^
a31e4dab85 Create renovate.json 2024-03-09 16:18:14 +08:00
2dust
b2a9397dc7 Revert "Merge pull request #2903 from ssszmath/master"
This reverts commit a536df1a02, reversing
changes made to 2dc82b7e83.
2024-03-07 08:22:28 +08:00
2dust
a536df1a02 Merge pull request #2903 from ssszmath/master
Fixing the problem of connecting to the Internet after publishing the application on Google Play in aab format
2024-03-07 08:06:04 +08:00
Sajad Sajad
1a2751563d add NativeLibs configuration to manifest and gradle.properties 2024-03-05 19:12:04 +03:30
2dust
2dc82b7e83 Merge pull request #2892 from vfarid/add_remark_to_custom_configs
Added remarks to all custom configs
2024-03-03 16:59:03 +08:00
2dust
b02d81ae53 Merge pull request #2889 from vfarid/more_fragment_options
More fragment options + Fix for reality configs
2024-03-03 16:53:22 +08:00
Vahid Farid
773bcc5658 Set V2rayConfig remarks based on ServerConfig in V2rayConfigUtil.kt 2024-03-02 18:30:05 +03:30
Vahid Farid
9142b9cfb4 Update AngConfigManager.kt
Added remark for single config subscription
Removed unnecessary code in multiple config subscription update
2024-03-02 18:27:59 +03:30
Vahid Farid
376882d975 Add remark to custom-config for export in ServerCustomConfigActivity.kt 2024-03-02 18:26:00 +03:30
Vahid Farid
f330c32ccc Get config name from custom-config's remark during import in MainViewModel.kt 2024-03-02 18:25:01 +03:30
Vahid Farid
58dbb71b53 Fix fragment for reality configs 2024-03-02 16:54:17 +03:30
Vahid Farid
ebd0257f6e Added new fragment packets for reality 2024-03-02 16:53:34 +03:30
2dust
945c584fc6 Merge pull request #2886 from MrMalekfar/master
Addressing issue #2880
2024-03-01 08:07:55 +08:00
MrMalekfar
19cd24c37a Update AngConfigManager.kt 2024-03-01 02:35:56 +03:30
MrMalekfar
3994810c4e Update AngConfigManager.kt 2024-03-01 02:06:36 +03:30
MrMalekfar
d86e68c77a Update AngConfigManager.kt 2024-03-01 01:51:08 +03:30
MrMalekfar
cc7bdefe54 Update AngConfigManager.kt 2024-03-01 01:27:59 +03:30
MrMalekfar
51b32a030a Update AngConfigManager.kt 2024-02-29 14:15:05 +03:30
MrMalekfar
6c76ddd145 Update AngConfigManager.kt 2024-02-29 14:12:19 +03:30
MrMalekfar
0d12cc5dc8 Update AngConfigManager.kt 2024-02-29 13:55:52 +03:30
MrMalekfar
cfb756723c Update AngConfigManager.kt 2024-02-29 11:07:39 +03:30
MrMalekfar
0e9f198341 Update AngConfigManager.kt 2024-02-29 09:59:58 +03:30
MrMalekfar
1fa1325630 Update AngConfigManager.kt 2024-02-29 09:50:31 +03:30
MrMalekfar
dc79d3a897 Update AngConfigManager.kt 2024-02-29 09:25:06 +03:30
MrMalekfar
486f3ffc96 Update AngConfigManager.kt 2024-02-29 09:23:46 +03:30
2dust
bf030e12f5 Merge pull request #2883 from admarty/master
Update vi translation
2024-02-29 08:58:12 +08:00
MrMalekfar
e80cce9696 Update AngConfigManager.kt 2024-02-29 02:04:42 +03:30
admarty
5bb9ecce47 Update vi translation 2024-02-28 22:32:26 +07:00
MrMalekfar
5eb09aa54e Update AngConfigManager.kt 2024-02-28 16:31:30 +03:30
MrMalekfar
ff261d9939 Update AngConfigManager.kt 2024-02-28 15:22:57 +03:30
MrMalekfar
1624ec87b2 Update AngConfigManager.kt 2024-02-28 15:06:37 +03:30
MrMalekfar
f1062f2f45 Update AngConfigManager.kt 2024-02-28 14:51:54 +03:30
2dust
39341c27bc up 1.8.17 2024-02-26 08:54:51 +08:00
2dust
31a90cec2b Merge pull request #2869 from Malus-risus/master
Refactor: Simplify Extensions and Improve Compatibility
2024-02-26 08:11:39 +08:00
Το μοχθηρό ^_^
617fc63393 Delete renovate.json 2024-02-25 19:59:53 +08:00
Το μοχθηρό ^_^
802f2cf3eb Update _Ext.kt 2024-02-25 19:05:48 +08:00
Το μοχθηρό ^_^
c7efcde868 Update _Ext.kt 2024-02-25 18:42:39 +08:00
Το μοχθηρό ^_^
e858179204 Update _Ext.kt 2024-02-25 18:31:42 +08:00
Το μοχθηρό ^_^
6871b0b950 Merge branch '2dust:master' into master 2024-02-25 10:42:48 +08:00
2dust
e9a27a1585 Merge pull request #2868 from NagisaEfi/master
Update translation
2024-02-25 09:28:36 +08:00
Το μοχθηρό ^_^
976b765629 Create renovate.json 2024-02-24 01:09:56 +08:00
NagisaEfi
126b9b6516 Update translation 2024-02-23 23:23:39 +08:00
2dust
94dab02b54 Merge pull request #2856 from Malus-risus/master
Remove unnecessary parentheses
2024-02-20 13:02:35 +08:00
Το μοχθηρό ^_^
a893b87730 Update MainRecyclerAdapter.kt 2024-02-19 14:36:28 +08:00
Το μοχθηρό ^_^
7db2ddd1f7 Update V2RayServiceManager.kt 2024-02-19 14:34:50 +08:00
Το μοχθηρό ^_^
748980aa1a Update PerAppProxyActivity.kt 2024-02-19 14:33:12 +08:00
Το μοχθηρό ^_^
96d416066e Update AppManagerUtil.kt 2024-02-19 14:31:47 +08:00
Το μοχθηρό ^_^
3955bb16bc Update _Ext.kt 2024-02-19 14:29:35 +08:00
Το μοχθηρό ^_^
de9bbf842f Update Utils.kt 2024-02-19 12:42:56 +08:00
Το μοχθηρό ^_^
336b673746 Update AngConfigManager.kt 2024-02-19 12:38:24 +08:00
Το μοχθηρό ^_^
f1b6b1e871 Update SettingsActivity.kt 2024-02-19 12:07:13 +08:00
2dust
fba4c03bb5 Bug fix 2024-02-17 09:55:26 +08:00
2dust
a32ae5b53f Merge pull request #2848 from NotDubious/master
Update Farsi translation
2024-02-17 09:23:11 +08:00
Mahyar
589e0f38fd Update Farsi translation 2024-02-16 11:07:31 +03:30
2dust
e21116680e Up 1.8.16 2024-02-16 13:48:41 +08:00
2dust
e1960f5aff Write remarks when pasting subscription link
https://github.com/2dust/v2rayNG/issues/2845
2024-02-16 09:52:10 +08:00
2dust
327ba57088 Improve 2024-02-16 09:39:39 +08:00
2dust
04e1b024e2 Merge pull request #2846 from Malus-risus/master
Update dependency
2024-02-16 09:20:25 +08:00
2dust
b59fe9b57b Delete renovate.json 2024-02-16 09:19:29 +08:00
2dust
fab0b756de Merge pull request #2844 from solokot/master
Update Russian translation
2024-02-16 09:12:59 +08:00
Το μοχθηρό ^_^
aa2727d0d0 Merge pull request #10 from Malus-risus/renovate/org.jetbrains.kotlinx-kotlinx-coroutines-android-1.x
Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-android to v1.8.0
2024-02-16 00:58:49 +08:00
Το μοχθηρό ^_^
ce3dd73a81 Merge branch '2dust:master' into master 2024-02-16 00:54:06 +08:00
renovate[bot]
2eab209fea Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-android to v1.8.0 2024-02-15 16:52:16 +00:00
Το μοχθηρό ^_^
02476657bf Merge pull request #11 from Malus-risus/renovate/org.jetbrains.kotlinx-kotlinx-coroutines-core-1.x
Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-core to v1.8.0
2024-02-16 00:51:46 +08:00
renovate[bot]
35602120e8 Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-core to v1.8.0 2024-02-15 15:36:59 +00:00
solokot
9e800e08ab Update Russian translation 2024-02-15 08:25:30 +03:00
2dust
09fc20794e Improve and translate 2024-02-14 19:40:51 +08:00
2dust
3cd95fbfdb Improve
https://github.com/2dust/v2rayNG/pull/2839
2024-02-14 18:52:05 +08:00
2dust
c243fadacf Merge pull request #2839 from vfarid/add_fragment_preferences
Added fragment to the preferences settings
2024-02-14 17:31:48 +08:00
Vahid Farid
9c06412ceb Update V2rayConfig.kt 2024-02-14 11:20:25 +03:30
Vahid Farid
8d1f0d5df9 Update ServerConfig.kt 2024-02-14 11:20:20 +03:30
Το μοχθηρό ^_^
a035f42008 Merge pull request #9 from Malus-risus/renovate/android-actions-setup-android-3.x
Update android-actions/setup-android action to v3
2024-02-14 15:40:03 +08:00
Το μοχθηρό ^_^
838fb041aa Merge pull request #8 from Malus-risus/renovate/actions-upload-artifact-4.x
Update actions/upload-artifact action to v4
2024-02-14 15:39:54 +08:00
Το μοχθηρό ^_^
7fbab63227 Merge pull request #7 from Malus-risus/renovate/actions-setup-java-4.x
Update actions/setup-java action to v4
2024-02-14 15:39:43 +08:00
Το μοχθηρό ^_^
ad871723fe Merge pull request #6 from Malus-risus/renovate/actions-setup-go-5.x
Update actions/setup-go action to v5
2024-02-14 15:39:33 +08:00
Το μοχθηρό ^_^
8887b44bf7 Merge pull request #5 from Malus-risus/renovate/actions-checkout-4.x
Update actions/checkout action to v4
2024-02-14 15:39:18 +08:00
Το μοχθηρό ^_^
176beced3a Merge pull request #4 from Malus-risus/renovate/gradle-8.x
Update dependency gradle to v8.6
2024-02-14 15:39:01 +08:00
Vahid Farid
a3561ddc6c Fixed and turened off fragmentOutbound function for future use 2024-02-14 11:06:41 +03:30
renovate[bot]
097bd06021 Update android-actions/setup-android action to v3 2024-02-14 07:30:38 +00:00
renovate[bot]
dfdd1efcc8 Update actions/upload-artifact action to v4 2024-02-14 07:30:35 +00:00
renovate[bot]
1ac5a410d4 Update actions/setup-java action to v4 2024-02-14 07:30:32 +00:00
renovate[bot]
8c44e849c9 Update actions/setup-go action to v5 2024-02-14 07:30:29 +00:00
renovate[bot]
2684bd2af4 Update actions/checkout action to v4 2024-02-14 07:30:26 +00:00
renovate[bot]
01a860aab5 Update dependency gradle to v8.6 2024-02-14 07:30:22 +00:00
Το μοχθηρό ^_^
e1faf4e54e Merge pull request #1 from Malus-risus/renovate/configure
Configure Renovate
2024-02-14 15:13:34 +08:00
renovate[bot]
14dd4d6b99 Add renovate.json 2024-02-14 07:12:20 +00:00
Vahid Farid
bab21bc8a5 Implemented fragment in V2rayConfigUtil.kt 2024-02-13 18:37:48 +03:30
Vahid Farid
8393b3ce86 Added relevant codes to handle fragment settings in SettingsActivity.kt 2024-02-13 18:36:40 +03:30
Vahid Farid
e15eec9cff Added fragment fields to pref_settings.xml 2024-02-13 18:35:46 +03:30
Vahid Farid
32741ed7ab Added PREF_FRAGMENT_* to SettingsViewModel.kt 2024-02-13 16:44:17 +03:30
Vahid Farid
0d77a65bbb Make tag, mux & sockopt writable in V2rayConfig.kt 2024-02-13 16:43:29 +03:30
Vahid Farid
6eaac2d7e9 Added getFragmentOutbound function to ServerConfig.kt 2024-02-13 16:38:27 +03:30
Vahid Farid
496a0ec92c Added PREF_FRAGMENT_* to AppConfig.kt 2024-02-13 16:37:51 +03:30
Vahid Farid
826329e996 Added title_pref_fragment_* to strings.xml 2024-02-13 16:29:39 +03:30
Vahid Farid
4f57da4a38 Added fragment_packets to arrays.xml 2024-02-13 16:28:46 +03:30
2dust
b3f49d0a34 Merge pull request #2836 from vfarid/fix_fragmentBean_add_dialerProxy
fix for SockoptBean & FragmentBean
2024-02-13 10:18:41 +08:00
2dust
b78b370408 Merge pull request #2834 from hamidrezahy/thirdparty_asset_update
Automatic update of third party dat files - fix #2821
2024-02-13 10:09:49 +08:00
Vahid Farid
82afcdddd0 Added dialerProxy to SockoptBean and fixed default values if FragmentBean 2024-02-12 17:51:27 +03:30
hamidreza hematyar
a33a698f38 handle upload file, improve asset ui, fix messages 2024-02-12 15:25:05 +03:30
hamidreza hematyar
3aff1800cd fix download assets functionality, fix strings.xml 2024-02-12 03:16:57 +03:30
hamidreza hematyar
ea816ca981 add asset by url menu, ui and save logic 2024-02-12 00:37:07 +03:30
hamidreza hematyar
a2bace4ede update strings.xml 2024-02-12 00:34:49 +03:30
2dust
5589d1058b Merge pull request #2831 from hamidrezahy/fix_translations
Add missing translations
2024-02-11 09:00:57 +08:00
hamidreza hematyar
a12fc32ff0 Add missing translations 2024-02-10 21:57:21 +03:30
2dust
ce2d1c5e0d Bug fix 2024-02-10 11:59:00 +08:00
2dust
eb75666c85 Merge pull request #2827 from vfarid/master
added support for multiple custom configs in subscriptions + remarks
2024-02-10 09:54:47 +08:00
2dust
4b970cedcc Up build.gradle.kts 2024-02-10 09:51:29 +08:00
Vahid Farid
fbd9d92f5e Update strings.xml 2024-02-10 01:22:07 +03:30
Vahid Farid
cdaff4da06 added support for multiple custom configs in subscriptions + remarks
Accepting both single config and multiple configs i.e:
{customConfig} or [{customConfig1}, {customConfig2} ...]

Furtheremore added an optional 'remarks' field to v2rayConfig class to keep remote customConfig name.
2024-02-09 20:25:51 +03:30
2dust
fbb17390f2 Up build.gradle.kts 2024-02-09 17:38:03 +08:00
2dust
52273db482 build.gradle 2 build.gradle.kts 2024-02-05 19:47:30 +08:00
2dust
a6af25ae88 up 1.8.15 2024-02-04 16:49:12 +08:00
2dust
3ac89d68fb Up implementation 2024-02-04 16:46:01 +08:00
2dust
267a43fd97 Refactor 2024-02-04 12:28:58 +08:00
2dust
6d0384b6f1 Bug fix 2024-02-02 16:47:22 +08:00
2dust
7ae4be402f Merge pull request #2785 from maskedeken/fix-wg
MUX should be disabled when using wireguard
2024-01-25 16:26:41 +08:00
2dust
46f0b7be5b Merge pull request #2783 from admarty/master
Update Vietnamese translation.
2024-01-25 16:26:19 +08:00
Eken Chan
b253f2d947 MUX should be disabled when using wireguard 2024-01-24 16:15:44 +08:00
admarty
444ade8afe Update Vietnamese translation 2024-01-23 19:27:46 +07:00
admarty
522dbdd170 Update Vietnamese translation. 2024-01-23 19:00:37 +07:00
2dust
d89238e1aa up 1.8.14 2024-01-19 18:56:44 +08:00
2dust
52710020ea update subscription 4 custom configuration 2024-01-19 17:44:35 +08:00
2dust
a18b8f4f2b Bug fix 2024-01-19 10:29:43 +08:00
2dust
01e0a6570d Merge pull request #2768 from solokot/master
Update Russian translation
2024-01-19 08:36:22 +08:00
solokot
cd6ef8f062 Update Russian translation
MTU menu
2024-01-18 08:38:45 +03:00
2dust
ea383c43bd Add share 4 Wireguard 2024-01-18 11:56:03 +08:00
2dust
68e08e3866 Merge pull request #2763 from admarty/master
Update vi translation
2024-01-18 09:57:21 +08:00
admarty
329ed26e85 Update vi translation 2024-01-17 12:56:39 +07:00
admarty
c830a4cea4 Revert "Updated vi translation"
This reverts commit e7e088ec83.
2024-01-17 12:53:53 +07:00
admarty
e7e088ec83 Updated vi translation 2024-01-17 12:46:48 +07:00
2dust
cf6c814eb4 Add share 4 Wireguard 2024-01-16 18:49:00 +08:00
2dust
8f62e42ae8 Add Wireguard Mtu 2024-01-16 17:48:04 +08:00
2dust
89b4060ba2 Merge pull request #2748 from admarty/master
Improve vietnamese translation
2024-01-11 17:53:51 +08:00
admarty
e4d4b329a0 fix typo 2024-01-11 13:03:53 +07:00
admarty
09c5f41995 Improve Vietnamese translation 2024-01-11 12:04:57 +07:00
admarty
02b25787c1 Improve Vietnamese translation 2024-01-11 00:48:28 +07:00
2dust
cdb050b6c5 up 1.8.13 2024-01-08 09:13:42 +08:00
2dust
21f47b536d Bug fix 2024-01-07 17:15:09 +08:00
2dust
f14484c986 Merge pull request #2733 from solokot/master
Russian translation
2024-01-07 16:31:38 +08:00
solokot
358dd78dc3 Russian translation
Small cosmetic fixes
2024-01-07 06:58:48 +00:00
2dust
d79e072316 Merge pull request #2732 from admarty/master
Improve Vietnamese translation
2024-01-07 09:25:27 +08:00
2dust
c24dee567a Modify background color 2024-01-07 09:24:48 +08:00
admarty
9f75bafcea Update strings.xml 2024-01-06 21:11:34 +07:00
admarty
2150452629 Update strings.xml 2024-01-06 20:50:50 +07:00
admarty
507a32a08e Improve Vietnamese translation 2024-01-06 20:22:52 +07:00
2dust
44dee8e850 Add main interface filter cache 2024-01-06 16:56:19 +08:00
2dust
5e4d9246c2 Merge pull request #2721 from mosayeb-a/fix-bug
fix a bug(ConcurrentModificationException)
2024-01-05 12:12:22 +08:00
mosayeb
9fe7419467 fix a bug(ConcurrentModificationException) 2024-01-03 11:15:00 -08:00
2dust
41b2251dfe Merge pull request #2718 from yuhan6665/wireguard
Wireguard can configure tun address
2024-01-02 12:06:08 +08:00
yuhan6665
73fad43573 Handle IPv6 address correctly 2024-01-01 22:16:07 -05:00
yuhan6665
4676717582 Wireguard can configure tun address
local tun address is also used by Wireguard server to check against its "allowedIPs" setting
2024-01-01 21:49:54 -05:00
2dust
834766e6e7 Merge pull request #2704 from yuhan6665/wireguard
Add Wireguard maunal config
2023-12-24 16:32:26 +08:00
yuhan6665
e304dce347 Add Wireguard maunal config
No support for export and import via links yet
2023-12-23 23:51:09 -05:00
2dust
61654aefeb Merge pull request #2690 from user09283/patch-1
Update strings.xml
2023-12-18 15:49:06 +08:00
user09283
1664aaa25b Update strings.xml
Update Vietnamese language
2023-12-18 00:41:47 +07:00
2dust
68f1f64f3d Merge pull request #2674 from mmrabbani/patch-1
Add HTTP3 to ALPN options
2023-12-14 09:20:11 +08:00
2dust
7bad57ca52 Merge pull request #2671 from solokot/master
Update Russian translation
2023-12-14 09:18:57 +08:00
MMR
1c8e1f0993 Add HTTP3 to ALPN options 2023-12-13 16:01:12 +03:30
solokot
6037ae6fc4 Update Russian translation 2023-12-09 18:59:33 +03:00
2dust
dc1c5400b8 Add mux concurency setting 2023-12-07 17:49:09 +08:00
2dust
9ae4688171 Update build.yml 2023-12-05 17:52:45 +08:00
2dust
e3f39234b2 Bug fix 2023-12-05 16:51:31 +08:00
2dust
0df0b2d6ac Merge pull request #2662 from solokot/master
Update Russian
2023-12-05 16:43:00 +08:00
2dust
9ce96d0591 up 1.8.12 2023-12-04 17:13:03 +08:00
2dust
a7ef0618ba Adjust FOREGROUND_SERVICE_SPECIAL_USE 2023-12-04 17:11:53 +08:00
solokot
cc9b083e5d Update Russian 2023-12-04 10:00:31 +03:00
2dust
5af322552c Merge pull request #2660 from yuhan6665/xudp
Use Android ID as XUDP basekey
2023-12-04 12:14:38 +08:00
yuhan6665
fc852281dd Use Android ID as XUDP basekey
Since Xray 1.8.1, XUDP pass basekey as the global ID. It can maintain the same UDP port on the proxy server outbound.
To enable maximum UDP connectivity, client should pass the device unique ID in the environment variable.
2023-12-03 22:06:06 -05:00
2dust
0b2f036a22 Up targetSdkVersion 34 2023-12-04 10:55:26 +08:00
2dust
cf2becb5e9 Bug fix 2023-12-04 08:52:26 +08:00
2dust
82d8eba1b9 Merge pull request #2636 from hadi-norouzi/feature/update_sub
Fix minimum update interval for subscription auto update
2023-11-21 16:47:17 +08:00
Hadi Norouzi
286ad34d94 fix minimum subscription update interval 2023-11-20 21:25:35 +03:30
Hadi Norouzi
ff0bc6594d Merge branch 'master' into feature/update_sub 2023-11-20 21:23:38 +03:30
Hadi Norouzi
7b8113aef1 Merge remote-tracking branch 'origin/feature/update_sub' into feature/update_sub
# Conflicts:
#	V2rayNG/app/build.gradle
2023-11-20 21:21:24 +03:30
2dust
81d2ef5db5 Merge pull request #2421 from Fangliding/master
自己写了个小破自动编译action
2023-11-18 18:56:13 +08:00
2dust
bdea3ef88c up 1.8.11 2023-11-18 17:24:37 +08:00
2dust
c62c86fc29 Adjust UI 2023-11-18 16:39:04 +08:00
2dust
59f698f755 Merge pull request #2614 from solokot/master
Update Russian translation
2023-11-18 16:32:06 +08:00
solokot
9ac979006e Update Russian translation 2023-11-17 20:44:48 +03:00
2dust
da6291a965 Add menu for privacy policy 2023-11-17 16:29:32 +08:00
2dust
bf6555e57c Update CR.md 2023-11-17 14:15:29 +08:00
2dust
35b114220e Update CR.md 2023-11-17 10:15:10 +08:00
Hadi Norouzi
800bb6a4e9 Merge branch 'master' of https://github.com/2dust/v2rayNG 2023-11-15 23:51:10 +03:30
2dust
2c80521f5b Merge pull request #2605 from solokot/master
Update Russian translation
2023-11-15 19:40:00 +08:00
2dust
6351ce5991 up 1.8.10 2023-11-15 19:35:16 +08:00
2dust
683362f0ee Fix mux enabled 2023-11-15 19:17:02 +08:00
2dust
84fc909339 Adjust UI 2023-11-15 17:22:16 +08:00
solokot
74171e26db Update Russian translation
Automatic update subscriptions
2023-11-15 11:20:55 +03:00
2dust
f25c0cc890 Fix Automatic update subscriptions 2023-11-15 15:27:53 +08:00
2dust
29848053a4 Revert "Merge pull request #2603 from maskedeken/fix-mux"
This reverts commit 0d9856919e, reversing
changes made to 3dde6b0ca3.
2023-11-15 14:17:21 +08:00
2dust
0d9856919e Merge pull request #2603 from maskedeken/fix-mux
fix mux not working
2023-11-15 14:12:33 +08:00
Eken Chan
0ae7f2f7b3 fix mux not working 2023-11-14 15:42:00 +08:00
2dust
3dde6b0ca3 Fix Automatic update subscriptions 2023-11-14 09:32:05 +08:00
2dust
6b28208044 Fix Automatic update subscriptions 2023-11-13 18:00:00 +08:00
2dust
13f855e3c4 remove android.intent.category.LEANBACK_LAUNCHER 2023-11-13 12:42:57 +08:00
2dust
c870595e98 Merge pull request #2596 from solokot/master
Russian translation
2023-11-13 12:39:24 +08:00
2dust
ae19a3f68d Merge pull request #2471 from hadi-norouzi/feature/update_sub
add auto update subscriptions with interval
2023-11-13 12:38:54 +08:00
2dust
545afc41b3 Merge branch 'master' into feature/update_sub 2023-11-13 12:37:59 +08:00
solokot
7177d88144 Russian translation
Fix Google translate
2023-11-10 19:48:26 +03:00
2dust
294ed50afd Merge pull request #2592 from hossinasaadi/master
fix ServerActivity.kt toast
2023-11-10 15:19:28 +08:00
2dust
0105fe48f7 Merge pull request #2590 from yuhan6665/xudp
Add menu options for Mux
2023-11-10 15:18:44 +08:00
Hossin Asaadi
c7ff23e3d5 Update ServerActivity.kt
fix trojan, shadowsocks password toast
2023-11-08 16:30:04 +04:00
yuhan6665
23e9d7fde5 Add menu options for Mux 2023-11-05 23:16:01 -05:00
Hadi Norouzi
c93edd8875 Merge branch '2dust:master' into master 2023-10-05 20:13:50 +03:30
2dust
bde37e38a7 up 1.8.9 2023-09-30 16:29:37 +08:00
2dust
c401d63d2f Merge pull request #2543 from yuhan6665/rprx-ui
Unified base activity for status bar icon color
2023-09-30 14:26:28 +08:00
yuhan6665
52416dd43d Unified base activity for status bar icon color 2023-09-29 19:59:06 -04:00
2dust
59bd7128ae Merge pull request #2540 from MrIbrahem/patch-1
Update strings.xml
2023-09-29 13:58:58 +08:00
2dust
87f16467bb Merge pull request #2539 from MrIbrahem/update
Update strings.xml
2023-09-29 13:58:46 +08:00
ibrahem Qasim
bed0fd00bd Update strings.xml
update arabic string
2023-09-28 00:32:18 +03:00
ibrahem Qasim
0672af98f8 Update strings.xml 2023-09-28 00:23:40 +03:00
2dust
2341eceb65 up 1.8.8 2023-09-25 11:32:57 +08:00
2dust
ec5f7245bf up 1.8.7 2023-09-25 11:20:21 +08:00
Hadi Norouzi
caa2edcf05 bump version 2023-09-12 22:32:41 +03:30
Hadi Norouzi
dbe78d0aa5 Merge remote-tracking branch 'origin/master' into feature/update_sub 2023-09-12 22:28:15 +03:30
Hadi Norouzi
71f2f590a7 build a version from auto update subscription 2023-09-12 22:20:28 +03:30
Hadi Norouzi
ed26120581 Merge branch '2dust:master' into master 2023-09-12 22:08:12 +03:30
风扇滑翔翼
71bd684b46 Update build.yml 2023-09-08 23:51:26 +08:00
风扇滑翔翼
02ae19f0c7 Merge remote-tracking branch 'upstream/master' 2023-09-08 15:25:46 +00:00
2dust
a46b0d58eb Merge pull request #2500 from MrIbrahem/master
Update arrays.xml to support Arabic language
2023-09-06 07:21:10 +08:00
ibrahem Qasim
d1262d169b Update arrays.xml 2023-09-01 17:39:16 +03:00
2dust
4a85c95b02 Merge pull request #2485 from y67h41/fix-/0-CIDR
Make /0 CIDR parsed as IP
2023-09-01 22:33:13 +08:00
2dust
a368927b9f Merge pull request #2475 from user09283/master-1
Update strings.xml
2023-09-01 22:31:51 +08:00
2dust
80feb69af5 Merge pull request #2460 from MrIbrahem/master
Create arabic translations
2023-09-01 22:31:38 +08:00
2dust
c09f0d5787 Merge pull request #2478 from yuhan6665/rprx-ui
A freshing new UI
2023-09-01 22:31:13 +08:00
y67h41
ff667546b8 Update ExampleUnitTest.kt 2023-08-24 17:55:12 +08:00
y67h41
6a255cdfa4 Make /0 CIDR be parsed as IP 2023-08-24 17:36:49 +08:00
yuhan6665
2b5784df6f Update UI: status bar icon color 2023-08-20 22:09:30 -04:00
user09283
a9665dd2d8 Update strings.xml 2023-08-19 13:11:14 +07:00
user09283
1e52877e93 Update strings.xml
Update Vietnamese language
2023-08-19 13:10:08 +07:00
Hadi Norouzi
062c0d8ddb add auto update subscriptions with interval 2023-08-17 21:06:22 +03:30
yuhan6665
a7a70b448f Update UI: tweak fab color 2023-08-15 20:14:43 -04:00
yuhan6665
3e52aeb804 Update UI: custom font 2023-08-13 23:10:20 -04:00
yuhan6665
4b4f5d145b Update UI: a concept for launching icon 2023-08-13 15:01:07 -04:00
风扇滑翔翼
775fa5ea62 update and remove unnecessary step 2023-08-12 00:54:19 +08:00
ibrahem Qasim
34d8329c8a Delete plurals.xml 2023-08-10 21:12:02 +03:00
ibrahem Qasim
2e7ae732aa Update strings.xml 2023-08-10 21:10:10 +03:00
ibrahem Qasim
424287e258 Rename s.xml to strings.xml 2023-08-10 13:26:25 +03:00
ibrahem Qasim
40d03bbb96 Delete strings.xml 2023-08-10 13:26:05 +03:00
ibrahem Qasim
d768694445 Update s.xml 2023-08-10 13:18:58 +03:00
ibrahem Qasim
56c7f2ef69 Update s.xml 2023-08-10 13:13:15 +03:00
ibrahem Qasim
3839b0c59c Update s.xml 2023-08-10 13:07:36 +03:00
ibrahem Qasim
77cab14ae8 Create s.xml 2023-08-10 12:58:37 +03:00
ibrahem Qasim
b0dbd4c7ca Create plurals.xml 2023-08-10 12:38:18 +03:00
ibrahem Qasim
50ca2e0e69 Create strings.xml 2023-08-10 12:36:43 +03:00
yuhan6665
0bf8beda94 Update UI: tweak after review 2023-08-09 21:38:20 -04:00
yuhan6665
4d334929c9 Update UI: complete change for dark mode 2023-08-07 14:32:41 -04:00
yuhan6665
ccfdf096f9 Update UI dark mode 2023-08-06 22:44:57 -04:00
yuhan6665
b4f2af2778 Update UI: header image 2023-08-06 21:53:14 -04:00
yuhan6665
32b9e4855c Update UI: clean up theme 2023-08-06 18:41:36 -04:00
yuhan6665
bdbce5147e Update UI: menu icons 2023-08-06 17:37:24 -04:00
yuhan6665
a4833506ae Update UI: launching icon 2023-08-06 16:55:29 -04:00
yuhan6665
d4e8072248 Update UI for white theme 2023-08-06 16:09:14 -04:00
yuhan6665
207ca93735 Fix lint by adding missing translation 2023-08-06 10:31:37 -04:00
2dust
99307ab8f0 up 1.8.6 2023-07-30 11:05:31 +08:00
风扇滑翔翼
a5bb39ac8a 允许手动指定编译时的Xray core版本
and update sth
2023-07-23 11:16:06 +08:00
风扇滑翔翼
073c7c0410 新增触发条件 2023-07-23 03:29:07 +08:00
风扇滑翔翼
7e88e3ba4f 新建Build action 2023-07-23 03:23:02 +08:00
2dust
1ec23a7b39 Merge pull request #2302 from dep4/fix/package-name
AppConfig: use current package name to receive broadcasts
2023-05-23 06:15:07 +08:00
Bruce Mills
685f9e220c AppConfig: use current package name to receive broadcasts 2023-05-21 15:54:25 -04:00
2dust
e77b7eb52e Merge pull request #2290 from CUMOON/master
Correction of Persian translation
2023-05-21 08:35:38 +08:00
2dust
29014704e0 Merge pull request #2288 from hadi-norouzi/feature/tv
feature: add leanback for android tv compatiblity
2023-05-21 08:35:13 +08:00
Moon
b3570d9c0b Correction of Persian translation 2023-05-17 12:49:41 +03:30
Hadi Norouzi
6e427cee82 feature: add leanback for android tv compatiblity 2023-05-16 11:33:29 +03:30
2dust
0a1b6d00d9 Merge pull request #2284 from solokot/master
RU: Improvements for google translation
2023-05-15 20:24:19 +08:00
solokot
00ffe66f36 RU: Improvements for google translation 2023-05-15 09:13:11 +03:00
2dust
63661fbdaa Merge pull request #2273 from hadi-norouzi/master
[Feature] separate package names for pre release and dev builds
2023-05-13 19:25:53 +08:00
Hadi Norouzi
4d4a4543c5 feature: import subscription from v2rayng://install-sub deeplink 2023-05-11 20:42:15 +03:30
Hadi Norouzi
2349805968 feature: separate package names for pre release and dev builds 2023-05-10 20:16:11 +03:30
2dust
bdbd0b154e up 1.8.5 2023-05-07 15:35:42 +08:00
2dust
653137ec58 bug fixes 2023-05-07 15:34:11 +08:00
2dust
39f65850be Merge pull request #2258 from Amir-yazdanmanesh/master
Fix persian translation
2023-05-07 15:19:33 +08:00
2dust
2d8db97417 Merge branch 'master' into master 2023-05-07 15:19:25 +08:00
2dust
17a6f80798 Merge pull request #2255 from hadi-norouzi/feature/share_sub
Feature/share sub
2023-05-07 15:18:28 +08:00
2dust
381fe859ff Merge pull request #2254 from hadi-norouzi/master
Fix status bar and navigation bar, remove FooterView TouchHelper
2023-05-07 15:14:04 +08:00
am.yazdanmanesh
e2bdf17b82 Fix persian translation 2023-05-05 21:54:18 +03:30
Hadi Norouzi
d20afc6801 Share subscription implemented 2023-05-04 18:03:50 +03:30
Hadi Norouzi
54046a27e6 Fix fa strings and test layout direction 2023-05-04 17:04:35 +03:30
Hadi Norouzi
36c000d18a Fix status bar and navigation bar, remove FooterView TouchHelper 2023-05-04 16:34:41 +03:30
2dust
2f3c2cf4d5 up 1.8.4 2023-04-29 20:46:28 +08:00
2dust
892358d5d8 Merge pull request #2227 from AlirezaIvaz/master
Add themed icon support
2023-04-27 09:40:08 +08:00
Alireza Ivaz
6dced903cd Add themed icon support 2023-04-26 17:05:11 +03:30
2dust
f8a98a426e Merge pull request #2213 from solokot/master
Updated Russian translation
2023-04-22 14:40:57 +08:00
solokot
011506e99f Updated Russian translation 2023-04-21 21:33:22 +03:00
2dust
966151d3fe bug fixes 2023-04-19 19:45:11 +08:00
2dust
247f4db77e up 1.8.3 2023-04-18 09:51:49 +08:00
2dust
9a661bc401 Use Google code scanner 2023-04-17 10:28:34 +08:00
2dust
f5f1b3816c Refactor createQRCode 2023-04-16 19:58:51 +08:00
2dust
3971c9badc use Exception 2023-04-16 19:56:44 +08:00
2dust
390fbf046b Merge pull request #2130 from pouriaksrvi/master
[New Feature] UX enhancement - Reversed the order of server list
2023-04-06 08:28:54 +08:00
Programmer
ec7ba59528 Bug Fix - Fixed the order of subscription import
Subscription server list should not be reversed when it is being imported. Fixed this problem.
2023-04-03 04:12:01 +03:30
Programmer
838941c6a4 Merge branch '2dust:master' into master 2023-04-03 04:00:52 +03:30
2dust
a0ec764b64 Merge pull request #2142 from solokot/master
Updated Russian translation
2023-04-02 09:32:29 +08:00
2dust
26f1f7099b up 1.8.2 2023-04-01 18:08:11 +08:00
2dust
613e9ac8bf Add permission POST_NOTIFICATIONS and READ_MEDIA_IMAGES 2023-03-31 13:48:40 +08:00
2dust
ea979f02b5 update gradle 2023-03-30 20:45:37 +08:00
2dust
383f55f809 update org.jetbrains.kotlin.android to 1.8.0 2023-03-30 13:21:36 +08:00
2dust
ce61f177dc Adjust build gradle 2023-03-30 11:54:15 +08:00
2dust
6a64157537 update kotlinx-coroutines to 1.6.4 2023-03-29 16:15:43 +08:00
2dust
de83302c8a update buildtoolsversion & targetSdkversion to 33 2023-03-29 15:54:35 +08:00
2dust
68553d3807 update kotlin version to 1.7.20 2023-03-29 15:25:54 +08:00
solokot
4cfe3394b1 Updated Russian translation 2023-03-29 09:35:28 +03:00
Programmer
ae9aa75ba0 New Feature
Fixed incorrect default order of server list as described in the issue https://github.com/2dust/v2rayNG/issues/2116
The order was problematic for users who add new servers more frequently
2023-03-26 06:27:52 +00:00
2dust
3d7ed12d4b Added the function of deleting duplicate configurations 2023-03-25 21:16:21 +08:00
2dust
f70be5bce9 routing domainMatcher is set to null 2023-03-25 18:10:29 +08:00
2dust
405667697e REALITY show false 2023-03-25 18:09:53 +08:00
2dust
3aeda7de81 Merge pull request #2122 from justlovediaodiao/master
fix a crash when adding custom config
2023-03-24 09:15:01 +08:00
yangxing
8a775d662a fix a crash when adding empty custom config 2023-03-23 22:25:07 +08:00
2dust
ec0ccbca76 up 1.8.1 2023-03-21 17:17:51 +08:00
2dust
6cdcbb0096 Add REALITY share link 2023-03-21 10:04:03 +08:00
2dust
79e3881704 hide alpn in reality 2023-03-21 09:19:21 +08:00
2dust
4cf2d429f0 Adjust string 2023-03-21 09:18:46 +08:00
2dust
fa51952bfa up 1.8.0 2023-03-10 11:17:25 +08:00
2dust
b2c4c0a67e Adjust string 2023-03-10 10:00:34 +08:00
2dust
cf2c58637b Merge pull request #2036 from yuhan6665/reality
Add reality and remove legacy xtls settings
2023-02-21 20:26:58 +08:00
yuhan6665
a2c262441e Add reality and remove legacy xtls settings
No sharing support for Reality yet
2023-02-20 21:25:33 -05:00
214 changed files with 6832 additions and 2745 deletions

63
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
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 19 -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/

31
CR.md
View File

@@ -1,29 +1,40 @@
v2rayNG 隐私条款
**v2rayNG 隐私权政策**
最后更新 2017-11-22
本政策自2023年11月17日起施行
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私条款。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
2dust 将 v2rayNG 应用程序构建为开源应用程序。 本服务由 2dust 免费提供,并且旨在按原样使用。
1. 信息收集
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私权政策。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私条款。
**信息收集**
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私权政策。
v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
2. 信息共享
**信息共享**
我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
3. 信息存留
**信息存留**
除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
4. 信息泄露
**信息泄露**
我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
5. 条款修改
**儿童隐私**
这些服务不针对 13 岁以下的任何人。我不会故意收集 13 岁以下儿童的个人身份信息。 如果我发现 13 岁以下的儿童向我提供了个人信息,我会立即从我们的服务器中删除该信息。 如果您是父母或监护人,并且您知道您的孩子向我们提供了个人信息,请与我联系,以便我能够采取必要的行动。
**条款修改**
我们保留修改这份隐私权政策的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私权政策。**
**联系我们**
如果您对我的隐私政策有任何疑问或建议,请随时通过 CaptainIronng@protonmail.com 与我联系。
我们保留修改这份隐私条款的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。**

View File

@@ -1,151 +0,0 @@
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 502
versionName "1.7.38"
}
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' }
// }
//}

View File

@@ -0,0 +1,139 @@
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 = 575
versionName = "1.8.31"
multiDexEnabled = true
splits.abi {
reset()
include(
"arm64-v8a",
"armeabi-v7a",
"x86_64",
"x86"
)
}
}
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()
}
splits {
abi {
isEnable = true
isUniversalApk = true
}
}
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
}
packagingOptions {
jniLibs {
useLegacyPackaging = true
}
}
}
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar","*.jar"))))
testImplementation("junit:junit:4.13.2")
implementation("com.google.android.flexbox:flexbox:3.0.0")
// Androidx
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.fragment:fragment-ktx:1.8.1")
implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.viewpager2:viewpager2:1.1.0")
// Androidx ktx
implementation("androidx.activity:activity-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.3")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
//kotlin
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.23")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
implementation("com.tencent:mmkv-static:1.3.4")
implementation("com.google.code.gson:gson:2.11.0")
implementation("io.reactivex:rxjava:1.3.8")
implementation("io.reactivex:rxandroid:1.2.1")
implementation("com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar")
implementation("me.drakeet.support:toastcompat:1.1.0")
implementation("com.blacksquircle.ui:editorkit:2.9.0")
implementation("com.blacksquircle.ui:language-base:2.9.0")
implementation("com.blacksquircle.ui:language-json:2.9.0")
implementation("io.github.g00fy2.quickie:quickie-bundled:1.9.0")
implementation("com.google.zxing:core:3.5.3")
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("androidx.work:work-multiprocess:2.8.1")
}

View File

@@ -1,13 +0,0 @@
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);
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="app_name" type="string">v2rayNG (DEV)</item>
</resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.v2ray.ang">
tools:ignore="MissingLeanbackLauncher">
<supports-screens
android:anyDensity="true"
@@ -10,8 +10,12 @@
android:largeScreens="true"
android:xlargeScreens="true"/>
<uses-sdk android:minSdkVersion="21" 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 -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
@@ -23,12 +27,19 @@
<!-- <useapplications-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
<uses-permission android:name="android.permission.CAMERA" />
<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.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<application
android:name=".AngApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:banner="@mipmap/ic_banner"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppThemeDayNight"
@@ -43,6 +54,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
@@ -81,6 +93,9 @@
<activity
android:exported="false"
android:name=".ui.UserAssetActivity" />
<activity
android:exported="false"
android:name=".ui.UserAssetUrlActivity" />
<activity
android:exported="false"
@@ -107,10 +122,14 @@
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<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>
</activity>
<activity
android:exported="false"
android:name=".ui.AboutActivity" />
<service
android:name=".service.V2RayVpnService"
@@ -118,6 +137,7 @@
android:exported="false"
android:label="@string/app_name"
android:permission="android.permission.BIND_VPN_SERVICE"
android:foregroundServiceType="specialUse"
android:process=":RunSoLibV2RayDaemon">
<intent-filter>
<action android:name="android.net.VpnService" />
@@ -125,12 +145,19 @@
<meta-data
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="true" />
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="vpn" />
</service>
<service android:name=".service.V2RayProxyOnlyService"
android:exported="false"
android:label="@string/app_name"
android:foregroundServiceType="specialUse"
android:process=":RunSoLibV2RayDaemon">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="proxy" />
</service>
<service android:name=".service.V2RayTestService"
@@ -157,11 +184,15 @@
android:name=".service.QSTileService"
android:icon="@drawable/ic_stat_name"
android:label="@string/app_tile_name"
android:foregroundServiceType="specialUse"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:process=":RunSoLibV2RayDaemon">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="tile" />
</service>
<!-- =====================Tasker===================== -->
<activity
@@ -183,6 +214,28 @@
</intent-filter>
</receiver>
<!-- =====================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>

View File

@@ -1,132 +1,2 @@
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,
geosite:cn,
geosite:geolocation-cn

View File

@@ -1,33 +1 @@
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,
geosite:geolocation-!cn

View File

@@ -1,28 +1,44 @@
package com.v2ray.ang
import android.content.Context
import androidx.multidex.MultiDexApplication
import androidx.preference.PreferenceManager
import androidx.work.Configuration
import com.tencent.mmkv.MMKV
import com.v2ray.ang.util.Utils
class AngApplication : MultiDexApplication() {
class AngApplication : MultiDexApplication(), Configuration.Provider {
companion object {
const val PREF_LAST_VERSION = "pref_last_version"
//const val PREF_LAST_VERSION = "pref_last_version"
lateinit var application: AngApplication
}
var firstRun = false
private set
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
application = this
}
//var firstRun = false
// private set
override fun onCreate() {
super.onCreate()
// LeakCanary.install(this)
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
if (firstRun)
defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
// val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
// firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
// if (firstRun)
// defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
//Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE)
MMKV.initialize(this)
Utils.setNightMode(application)
}
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder()
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
.build()
}
}

View File

@@ -5,43 +5,73 @@ package com.v2ray.ang
* App Config Const
*/
object AppConfig {
const val ANG_PACKAGE = "com.v2ray.ang"
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
const val DIR_ASSETS = "assets"
const val DIR_BACKUPS = "backups"
// legacy
const val ANG_CONFIG = "ang_config"
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
// Preferences mapped to MMKV
const val PREF_MODE = "pref_mode"
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
const val PREF_ROUTE_ONLY_ENABLED = "pref_route_only_enabled"
const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
const val PREF_BYPASS_APPS = "pref_bypass_apps"
const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
const val PREF_VPN_DNS = "pref_vpn_dns"
const val PREF_REMOTE_DNS = "pref_remote_dns"
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
const val PREF_SOCKS_PORT = "pref_socks_port"
const val PREF_HTTP_PORT = "pref_http_port"
const val PREF_LOGLEVEL = "pref_core_loglevel"
const val PREF_LANGUAGE = "pref_language"
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
const val PREF_VPN_DNS = "pref_vpn_dns"
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
const val PREF_ROUTING_MODE = "pref_routing_mode"
const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent"
const val PREF_V2RAY_ROUTING_DIRECT = "pref_v2ray_routing_direct"
const val PREF_V2RAY_ROUTING_BLOCKED = "pref_v2ray_routing_blocked"
const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
const val PREF_BYPASS_APPS = "pref_bypass_apps"
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
const val HTTP_PROTOCOL: String = "http://"
const val HTTPS_PROTOCOL: String = "https://"
const val PREF_MUX_ENABLED = "pref_mux_enabled"
const val PREF_MUX_CONCURRENCY = "pref_mux_concurency"
const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurency"
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" // 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_SOCKS_PORT = "pref_socks_port"
const val PREF_HTTP_PORT = "pref_http_port"
const val PREF_REMOTE_DNS = "pref_remote_dns"
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
const val PREF_LOGLEVEL = "pref_core_loglevel"
const val PREF_MODE = "pref_mode"
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
//Preferences mapped to MMKV End
const val PROTOCOL_HTTP: String = "http://"
const val PROTOCOL_HTTPS: String = "https://"
const val PROTOCOL_FREEDOM: String = "freedom"
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
@@ -53,23 +83,35 @@ object AppConfig {
const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
const val TASKER_DEFAULT_GUID = "Default"
const val TAG_AGENT = "proxy"
const val TAG_PROXY = "proxy"
const val TAG_DIRECT = "direct"
const val TAG_BLOCKED = "block"
const val TAG_FRAGMENT = "fragment"
const val androidpackagenamelistUrl = "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
const val v2rayCustomRoutingListUrl = "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
const val v2rayNGIssues = "https://github.com/2dust/v2rayNG/issues"
const val v2rayNGWikiMode = "https://github.com/2dust/v2rayNG/wiki/Mode"
const val promotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
const val geoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/";
const val 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"
const val DNS_AGENT = "1.1.1.1"
const val DNS_PROXY = "1.1.1.1"
const val DNS_DIRECT = "223.5.5.5"
const val DNS_VPN = "1.1.1.1"
const val PORT_LOCAL_DNS = "10853"
const val PORT_SOCKS = "10808"
const val PORT_HTTP = "10809"
const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32"
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
const val WIREGUARD_LOCAL_MTU = "1420"
const val MSG_REGISTER_CLIENT = 1
const val MSG_STATE_RUNNING = 11

View File

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

View File

@@ -0,0 +1,8 @@
package com.v2ray.ang.dto
data class AssetUrlItem(
var remarks: String = "",
var url: String = "",
val addedTime: Long = System.currentTimeMillis(),
var lastUpdated: Long = -1
)

View File

@@ -1,6 +1,6 @@
package com.v2ray.ang.dto
import com.v2ray.ang.AppConfig.TAG_AGENT
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.AppConfig.TAG_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.util.Utils
@@ -26,7 +26,7 @@ data class ServerConfig(
vnext = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())))),
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
EConfigType.CUSTOM, EConfigType.WIREGUARD ->
EConfigType.CUSTOM ->
return ServerConfig(configType = configType)
EConfigType.SHADOWSOCKS, EConfigType.SOCKS, EConfigType.TROJAN ->
return ServerConfig(
@@ -36,6 +36,15 @@ data class ServerConfig(
settings = V2rayConfig.OutboundBean.OutSettingsBean(
servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())),
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
EConfigType.WIREGUARD ->
return ServerConfig(
configType = configType,
outboundBean = V2rayConfig.OutboundBean(
protocol = configType.name.lowercase(),
settings = V2rayConfig.OutboundBean.OutSettingsBean(
secretKey = "",
peers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean())
)))
}
}
}
@@ -49,7 +58,7 @@ data class ServerConfig(
fun getAllOutboundTags(): MutableList<String> {
if (configType != EConfigType.CUSTOM) {
return mutableListOf(TAG_AGENT, TAG_DIRECT, TAG_BLOCKED)
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
}
fullConfig?.let { config ->
return config.outbounds.map { it.tag }.toMutableList()
@@ -60,10 +69,6 @@ data class ServerConfig(
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)
}
return Utils.getIpv6Address(address) + ":" + port
}
}

View File

@@ -4,5 +4,9 @@ data class SubscriptionItem(
var remarks: String = "",
var url: String = "",
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,
)

View File

@@ -10,6 +10,7 @@ import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
data class V2rayConfig(
var remarks: String? = null,
var stats: Any? = null,
val log: LogBean,
var policy: PolicyBean?,
@@ -21,7 +22,9 @@ data class V2rayConfig(
val transport: Any? = null,
val reverse: Any? = null,
var fakedns: Any? = null,
val browserForwarder: Any? = null) {
val browserForwarder: Any? = null,
var observatory: Any? = null,
var burstObservatory: Any? = null) {
companion object {
const val DEFAULT_PORT = 443
const val DEFAULT_SECURITY = "auto"
@@ -29,7 +32,7 @@ data class V2rayConfig(
const val DEFAULT_NETWORK = "tcp"
const val TLS = "tls"
const val XTLS = "xtls"
const val REALITY = "reality"
const val HTTP = "http"
}
@@ -57,24 +60,26 @@ data class V2rayConfig(
data class SniffingBean(var enabled: Boolean,
val destOverride: ArrayList<String>,
val metadataOnly: Boolean? = null)
val metadataOnly: Boolean? = null,
var routeOnly: Boolean? = null)
}
data class OutboundBean(val tag: String = "proxy",
data class OutboundBean(var tag: String = "proxy",
var protocol: String,
var settings: OutSettingsBean? = null,
var streamSettings: StreamSettingsBean? = null,
val proxySettings: Any? = null,
val sendThrough: String? = null,
val mux: MuxBean? = MuxBean(false)) {
var mux: MuxBean? = MuxBean(false)) {
data class OutSettingsBean(var vnext: List<VnextBean>? = null,
var fragment: FragmentBean? = null,
var servers: List<ServersBean>? = null,
/*Blackhole*/
var response: Response? = null,
/*DNS*/
val network: String? = null,
val address: Any? = null,
var address: Any? = null,
val port: Int? = null,
/*Freedom*/
var domainStrategy: String? = null,
@@ -83,8 +88,10 @@ data class V2rayConfig(
/*Loopback*/
val inboundTag: String? = null,
/*Wireguard*/
val secretKey: String? = null,
var secretKey: String? = null,
val peers: List<WireGuardBean>? = null,
var reserved: List<Int>? = null,
var mtu :Int? = null
) {
data class VnextBean(var address: String = "",
@@ -99,6 +106,10 @@ data class V2rayConfig(
var flow: String = "")
}
data class FragmentBean(var packets: String? = null,
var length: String? = null,
var interval: String? = null)
data class ServersBean(var address: String = "",
var method: String = "chacha20-poly1305",
var ota: Boolean = false,
@@ -127,13 +138,15 @@ data class V2rayConfig(
var tcpSettings: TcpSettingsBean? = null,
var kcpSettings: KcpSettingsBean? = null,
var wsSettings: WsSettingsBean? = null,
var httpupgradeSettings: HttpupgradeSettingsBean? = null,
var splithttpSettings: SplithttpSettingsBean? = null,
var httpSettings: HttpSettingsBean? = null,
var tlsSettings: TlsSettingsBean? = null,
var quicSettings: QuicSettingBean? = null,
var xtlsSettings: TlsSettingsBean? = null,
var realitySettings: TlsSettingsBean? = null,
var grpcSettings: GrpcSettingsBean? = null,
val dsSettings: Any? = null,
val sockopt: Any? = null
var sockopt: SockoptBean? = null
) {
data class TcpSettingsBean(var header: HeaderBean = HeaderBean(),
@@ -145,7 +158,7 @@ data class V2rayConfig(
var headers: HeadersBean = HeadersBean(),
val version: String? = null,
val method: String? = null) {
data class HeadersBean(var Host: List<String> = ArrayList(),
data class HeadersBean(var Host: List<String>? = ArrayList(),
@SerializedName("User-Agent")
val userAgent: List<String>? = null,
@SerializedName("Accept-Encoding")
@@ -176,9 +189,24 @@ data class V2rayConfig(
data class HeadersBean(var Host: String = "")
}
data class HttpupgradeSettingsBean(var path: String = "",
var host: String = "",
val acceptProxyProtocol: Boolean? = null)
data class SplithttpSettingsBean(var path: String = "",
var host: String = "",
val maxUploadSize: Int? = null,
val maxConcurrentUploads: Int? = null)
data class HttpSettingsBean(var host: List<String> = ArrayList(),
var path: String = "")
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,
@@ -189,7 +217,12 @@ data class V2rayConfig(
val fingerprint: String? = null,
val certificates: List<Any>? = null,
val disableSystemRoot: Boolean? = null,
val enableSessionResumption: 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 = "",
@@ -198,10 +231,15 @@ data class V2rayConfig(
}
data class GrpcSettingsBean(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
)
fun populateTransportSettings(transport: String, headerType: String?, host: String?, path: String?, seed: String?,
quicSecurity: String?, key: String?, mode: String?, serviceName: String?): String {
quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
authority: String?): String {
var sni = ""
network = transport
when (network) {
@@ -214,7 +252,7 @@ data class V2rayConfig(
requestObj.headers.Host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
requestObj.path = (path ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
tcpSetting.header.request = requestObj
sni = requestObj.headers.Host.getOrNull(0) ?: sni
sni = requestObj.headers.Host?.getOrNull(0) ?: sni
}
} else {
tcpSetting.header.type = "none"
@@ -239,6 +277,20 @@ data class V2rayConfig(
wssetting.path = path ?: "/"
wsSettings = wssetting
}
"httpupgrade" -> {
val httpupgradeSetting = HttpupgradeSettingsBean()
httpupgradeSetting.host = host ?: ""
sni = httpupgradeSetting.host
httpupgradeSetting.path = path ?: "/"
httpupgradeSettings = httpupgradeSetting
}
"splithttp" -> {
val splithttpSetting = SplithttpSettingsBean()
splithttpSetting.host = host ?: ""
sni = splithttpSetting.host
splithttpSetting.path = path ?: "/"
splithttpSettings = splithttpSetting
}
"h2", "http" -> {
network = "h2"
val h2Setting = HttpSettingsBean()
@@ -258,32 +310,42 @@ data class V2rayConfig(
val grpcSetting = GrpcSettingsBean()
grpcSetting.multiMode = mode == "multi"
grpcSetting.serviceName = serviceName ?: ""
sni = host ?: ""
grpcSetting.authority = authority ?: ""
grpcSetting.idle_timeout = 60
grpcSetting.health_check_timeout = 20
sni = authority ?: ""
grpcSettings = grpcSetting
}
}
return sni
}
fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?) {
fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?,
publicKey: String?, shortId: String?, spiderX: String?) {
security = streamSecurity
val tlsSetting = TlsSettingsBean(
allowInsecure = allowInsecure,
serverName = sni,
fingerprint = fingerprint,
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() }
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
publicKey = publicKey,
shortId = shortId,
spiderX = spiderX
)
if (security == TLS) {
tlsSettings = tlsSetting
xtlsSettings = null
} else if (security == XTLS) {
realitySettings = null
} else if (security == REALITY) {
tlsSettings = null
xtlsSettings = tlsSetting
realitySettings = tlsSetting
}
}
}
data class MuxBean(var enabled: Boolean, var concurrency: Int = 8)
data class MuxBean(var enabled: Boolean,
var concurrency: Int = 8,
var xudpConcurrency: Int = 8,
var xudpProxyUDP443: String = "",)
fun getServerAddress(): String? {
if (protocol.equals(EConfigType.VMESS.name, true)
@@ -340,7 +402,8 @@ data class V2rayConfig(
fun getTransportSettingDetails(): List<String>? {
if (protocol.equals(EConfigType.VMESS.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
return when (transport) {
"tcp" -> {
@@ -361,6 +424,18 @@ data class V2rayConfig(
wsSetting.headers.Host,
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" -> {
val h2Setting = streamSettings?.httpSettings ?: return null
listOf("",
@@ -376,7 +451,7 @@ data class V2rayConfig(
"grpc" -> {
val grpcSetting = streamSettings?.grpcSettings ?: return null
listOf(if (grpcSetting.multiMode == true) "multi" else "gun",
"",
grpcSetting.authority ?: "",
grpcSetting.serviceName)
}
else -> null
@@ -405,7 +480,7 @@ data class V2rayConfig(
var rules: ArrayList<RulesBean>,
val balancers: List<Any>? = null) {
data class RulesBean(var type: String = "",
data class RulesBean(
var ip: ArrayList<String>? = null,
var domain: ArrayList<String>? = null,
var outboundTag: String = "",
@@ -438,8 +513,8 @@ data class V2rayConfig(
var poolSize: Int = 10000) // roughly 10 times smaller than total ip pool
fun getProxyOutbound(): OutboundBean? {
outbounds.forEach { outbound ->
EConfigType.values().forEach {
outbounds?.forEach { outbound ->
EConfigType.entries.forEach {
if (outbound.protocol.equals(it.name, true)) {
return outbound
}

View File

@@ -9,72 +9,61 @@ import org.json.JSONObject
import java.net.URI
import java.net.URLConnection
/**
* Some extensions
*/
val Context.v2RayApplication: AngApplication
get() = applicationContext as AngApplication
fun Context.toast(message: Int): Toast = ToastCompat
.makeText(this, message, Toast.LENGTH_SHORT)
.apply {
show()
fun Context.toast(message: Int) {
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).apply { show() }
}
fun Context.toast(message: CharSequence): Toast = ToastCompat
.makeText(this, message, Toast.LENGTH_SHORT)
.apply {
show()
fun Context.toast(message: CharSequence) {
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).apply { show() }
}
fun JSONObject.putOpt(pair: Pair<String, Any>) = putOpt(pair.first, pair.second)
fun JSONObject.putOpt(pairs: Map<String, Any>) = pairs.forEach { putOpt(it.key to it.value) }
fun JSONObject.putOpt(pair: Pair<String, Any?>) {
put(pair.first, pair.second)
}
const val threshold = 1000
const val divisor = 1024F
fun JSONObject.putOpt(pairs: Map<String, Any?>) {
pairs.forEach { put(it.key, it.value) }
}
fun Long.toSpeedString() = toTrafficString() + "/s"
const val THRESHOLD = 1000L
const val DIVISOR = 1024.0
fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
fun Long.toTrafficString(): String {
if (this == 0L)
return "\t\t\t0\t B"
if (this < threshold)
return "${this.toFloat().toShortString()}\t B"
val kib = this / divisor
if (kib < threshold)
return "${kib.toShortString()}\t KB"
val mib = kib / divisor
if (mib < threshold)
return "${mib.toShortString()}\t MB"
val gib = mib / divisor
if (gib < threshold)
return "${gib.toShortString()}\t GB"
val tib = gib / divisor
if (tib < threshold)
return "${tib.toShortString()}\t TB"
val pib = tib / divisor
if (pib < threshold)
return "${pib.toShortString()}\t PB"
return ""
if (this < THRESHOLD) {
return "$this B"
}
private fun Float.toShortString(): String {
val s = "%.2f".format(this)
if (s.length <= 4)
return s
return s.substring(0, 4).removeSuffix(".")
val kb = this / DIVISOR
if (kb < THRESHOLD) {
return "${String.format("%.1f KB", kb)}"
}
val mb = kb / DIVISOR
if (mb < THRESHOLD) {
return "${String.format("%.1f MB", mb)}"
}
val gb = mb / DIVISOR
if (gb < THRESHOLD) {
return "${String.format("%.1f GB", gb)}"
}
val tb = gb / DIVISOR
if (tb < THRESHOLD) {
return "${String.format("%.1f TB", tb)}"
}
return String.format("%.1f PB", tb / DIVISOR)
}
val URLConnection.responseLength: Long
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) contentLengthLong else contentLength.toLong()
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
contentLengthLong
} else {
contentLength.toLong()
}
val URI.idnHost: String
get() = (host!!).replace("[", "").replace("]", "")
get() = host?.replace("[", "")?.replace("]", "") ?: ""
fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "")

View File

@@ -4,7 +4,6 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.text.TextUtils
import com.google.zxing.WriterException
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.service.V2RayServiceManager
@@ -34,7 +33,7 @@ class TaskerReceiver : BroadcastReceiver() {
} else {
Utils.stopVService(context)
}
} catch (e: WriterException) {
} catch (e: Exception) {
e.printStackTrace()
}
}

View File

@@ -8,8 +8,8 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.widget.RemoteViews
import com.v2ray.ang.R
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.Utils
@@ -38,17 +38,11 @@ class WidgetProvider : AppWidgetProvider() {
})
remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent)
if (isRunning) {
remoteViews.setInt(
R.id.layout_switch,
"setBackgroundResource",
R.drawable.ic_rounded_corner_theme
)
remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_stop_24dp)
remoteViews.setInt(R.id.layout_background, "setBackgroundResource", R.drawable.ic_rounded_corner_active)
} else {
remoteViews.setInt(
R.id.layout_switch,
"setBackgroundResource",
R.drawable.ic_rounded_corner_grey
)
remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_play_24dp)
remoteViews.setInt(R.id.layout_background, "setBackgroundResource", R.drawable.ic_rounded_corner_inactive)
}
for (appWidgetId in appWidgetIds) {

View File

@@ -36,7 +36,12 @@ class QSTileService : TileService() {
super.onStartListening()
setState(Tile.STATE_INACTIVE)
mMsgReceive = ReceiveMessageHandler(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY), Context.RECEIVER_EXPORTED)
} else {
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
}
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
}

View File

@@ -0,0 +1,68 @@
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.R
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
object SubscriptionUpdater {
const val notificationChannel = "subscription_update_channel"
class UpdateTask(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
private val notificationManager = NotificationManagerCompat.from(applicationContext)
private val notification =
NotificationCompat.Builder(applicationContext, notificationChannel)
.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 (i in subs) {
val subscription = i.second
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notification.setChannelId(notificationChannel)
val channel =
NotificationChannel(
notificationChannel,
"Subscription Update Service",
NotificationManager.IMPORTANCE_MIN
)
notificationManager.createNotificationChannel(channel)
}
notificationManager.notify(3, notification.build())
Log.d(
AppConfig.ANG_PACKAGE,
"subscription automatic update: ---${subscription.remarks}"
)
val configs = Utils.getUrlContentWithCustomUserAgent(subscription.url)
AngConfigManager.importBatchConfig(configs, i.first, false)
notification.setContentText("Updating ${subscription.remarks}")
}
notificationManager.cancel(3)
return Result.success()
}
}
}

View File

@@ -1,6 +1,9 @@
package com.v2ray.ang.service
import android.app.*
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -50,7 +53,7 @@ object V2RayServiceManager {
set(value) {
field = value
Seq.setContext(value?.get()?.getService()?.applicationContext)
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()))
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
}
var currentConfig: ServerConfig? = null
@@ -60,6 +63,11 @@ object V2RayServiceManager {
private var mNotificationManager: NotificationManager? = null
fun startV2Ray(context: Context) {
if (v2rayPoint.isRunning) return
val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
val result = V2rayConfigUtil.getV2rayConfig(context, guid)
if (!result.status) return
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) {
context.toast(R.string.toast_warning_pref_proxysharing_short)
} else {
@@ -123,7 +131,9 @@ object V2RayServiceManager {
val service = serviceControl?.get()?.getService() ?: return
val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
val config = MmkvManager.decodeServerConfig(guid) ?: return
if (!v2rayPoint.isRunning) {
if (v2rayPoint.isRunning) {
return
}
val result = V2rayConfigUtil.getV2rayConfig(service, guid)
if (!result.status)
return
@@ -133,7 +143,11 @@ object V2RayServiceManager {
mFilter.addAction(Intent.ACTION_SCREEN_ON)
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
mFilter.addAction(Intent.ACTION_USER_PRESENT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
service.registerReceiver(mMsgReceive, mFilter, Context.RECEIVER_EXPORTED)
} else {
service.registerReceiver(mMsgReceive, mFilter)
}
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
}
@@ -156,7 +170,6 @@ object V2RayServiceManager {
cancelNotification()
}
}
}
fun stopV2rayPoint() {
val service = serviceControl?.get()?.getService() ?: return
@@ -230,11 +243,19 @@ object V2RayServiceManager {
var errstr = ""
if (v2rayPoint.isRunning) {
try {
time = v2rayPoint.measureDelay()
time = v2rayPoint.measureDelay(Utils.getDelayTestUrl())
} catch (e: Exception) {
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
errstr = e.message?.substringAfter("\":") ?: "empty message"
}
if (time == -1L) {
try {
time = v2rayPoint.measureDelay(Utils.getDelayTestUrl(true))
} catch (e: Exception) {
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
errstr = e.message?.substringAfter("\":") ?: "empty message"
}
}
}
val result = if (time == -1L) {
service.getString(R.string.connection_test_error, errstr)
@@ -286,7 +307,7 @@ object V2RayServiceManager {
.setShowWhen(false)
.setOnlyAlertOnce(true)
.setContentIntent(contentPendingIntent)
.addAction(R.drawable.ic_close_grey_800_24dp,
.addAction(R.drawable.ic_delete_24dp,
service.getString(R.string.notification_action_stop_v2ray),
stopV2RayPendingIntent)
//.build()
@@ -364,7 +385,7 @@ object V2RayServiceManager {
}
val directUplink = v2rayPoint.queryStats(TAG_DIRECT, "uplink")
val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, "downlink")
val zeroSpeed = (proxyTotal == 0L && directUplink == 0L && directDownlink == 0L)
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
if (!zeroSpeed || !lastZeroSpeed) {
if (proxyTotal == 0L) {
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)

View File

@@ -20,7 +20,7 @@ class V2RayTestService : Service() {
override fun onCreate() {
super.onCreate()
Seq.setContext(this)
Libv2ray.initV2Env(Utils.userAssetPath(this))
Libv2ray.initV2Env(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {

View File

@@ -111,7 +111,8 @@ class V2RayVpnService : VpnService(), ServiceControl {
val builder = Builder()
//val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
builder.setMtu(VPN_MTU)
builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)

View File

@@ -0,0 +1,177 @@
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.rxpermissions.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 lateinit var binding: ActivityAboutBinding
private val extDir by lazy { File(Utils.backupPath(this)) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAboutBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
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())
}
}
}
}

View File

@@ -2,13 +2,25 @@ package com.v2ray.ang.ui
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.view.MenuItem
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import com.v2ray.ang.util.MyContextWrapper
import com.v2ray.ang.util.Utils
abstract class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (!Utils.getDarkModeStatus(this)) {
WindowCompat.getInsetsController(window, window.decorView).apply {
isAppearanceLightStatusBars = true
}
}
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
android.R.id.home -> {
onBackPressed()

View File

@@ -31,7 +31,6 @@ class LogcatActivity : BaseActivity() {
title = getString(R.string.title_logcat)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
logcat(false)
}

View File

@@ -1,47 +1,50 @@
package com.v2ray.ang.ui
import android.Manifest
import android.content.*
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.res.ColorStateList
import android.net.Uri
import android.net.VpnService
import androidx.recyclerview.widget.LinearLayoutManager
import android.view.Menu
import android.view.MenuItem
import com.tbruyelle.rxpermissions.RxPermissions
import com.v2ray.ang.R
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.view.KeyEvent
import com.v2ray.ang.AppConfig
import android.content.res.ColorStateList
import com.google.android.material.navigation.NavigationView
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.recyclerview.widget.ItemTouchHelper
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
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.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.navigation.NavigationView
import com.tbruyelle.rxpermissions.RxPermissions
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityMainBinding
import com.v2ray.ang.databinding.LayoutProgressBinding
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.MainViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.drakeet.support.toast.ToastCompat
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.*
import com.v2ray.ang.viewmodel.MainViewModel
import kotlinx.coroutines.*
import me.drakeet.support.toast.ToastCompat
import java.io.File
import java.io.FileOutputStream
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
private lateinit var binding: ActivityMainBinding
@@ -68,7 +71,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
binding.fab.setOnClickListener {
if (mainViewModel.isRunning.value == true) {
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)
if (intent == null) {
startV2Ray()
@@ -102,11 +105,30 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
binding.drawerLayout.addDrawerListener(toggle)
toggle.syncState()
binding.navView.setNavigationItemSelectedListener(this)
binding.version.text = "v${BuildConfig.VERSION_NAME} (${SpeedtestUtil.getLibVersion()})"
setupViewModel()
copyAssets()
migrateLegacy()
mainViewModel.copyAssets(assets)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
RxPermissions(this)
.request(Manifest.permission.POST_NOTIFICATIONS)
.subscribe {
if (!it)
toast(R.string.toast_permission_denied)
}
}
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() {
@@ -121,66 +143,25 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
mainViewModel.isRunning.observe(this) { isRunning ->
adapter.isRunning = isRunning
if (isRunning) {
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorSelected))
binding.fab.setImageResource(R.drawable.ic_stop_24dp)
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_active))
setTestState(getString(R.string.connection_connected))
binding.layoutTest.isFocusable = true
} else {
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorUnselected))
binding.fab.setImageResource(R.drawable.ic_play_24dp)
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
setTestState(getString(R.string.connection_not_connected))
binding.layoutTest.isFocusable = false
}
hideCircle()
}
mainViewModel.startListenBroadcast()
}
private fun copyAssets() {
val extFolder = Utils.userAssetPath(this)
lifecycleScope.launch(Dispatchers.IO) {
try {
val geo = arrayOf("geosite.dat", "geoip.dat")
assets.list("")
?.filter { geo.contains(it) }
?.filter { !File(extFolder, it).exists() }
?.forEach {
val target = File(extFolder, it)
assets.open(it).use { input ->
FileOutputStream(target).use { output ->
input.copyTo(output)
}
}
Log.i(ANG_PACKAGE, "Copied from apk assets folder to ${target.absolutePath}")
}
} catch (e: Exception) {
Log.e(ANG_PACKAGE, "asset copy failed", e)
}
}
}
private fun migrateLegacy() {
lifecycleScope.launch(Dispatchers.IO) {
val result = AngConfigManager.migrateLegacyConfig(this@MainActivity)
if (result != null) {
launch(Dispatchers.Main) {
if (result) {
toast(getString(R.string.migration_success))
mainViewModel.reloadServerList()
} else {
toast(getString(R.string.migration_fail))
}
}
}
}
}
fun startV2Ray() {
if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
return
}
showCircle()
// toast(R.string.toast_services_start)
V2RayServiceManager.startV2Ray(this)
hideCircle()
}
fun restartV2Ray() {
@@ -237,6 +218,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
importManually(EConfigType.TROJAN.value)
true
}
R.id.import_manually_wireguard -> {
importManually(EConfigType.WIREGUARD.value)
true
}
R.id.import_config_custom_clipboard -> {
importConfigCustomClipboard()
true
@@ -254,11 +239,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
true
}
// R.id.sub_setting -> {
// startActivity<SubSettingActivity>()
// true
// }
R.id.sub_update -> {
importConfigViaSub()
true
@@ -294,16 +274,33 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
MmkvManager.removeAllServer()
mainViewModel.reloadServerList()
}
.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) { _, _ ->
mainViewModel.removeDuplicateServer()
mainViewModel.reloadServerList()
}
.setNegativeButton(android.R.string.no) {_, _ ->
//do noting
}
.show()
true
}
R.id.del_invalid_config -> {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
MmkvManager.removeInvalidServer()
mainViewModel.reloadServerList()
}
.setNegativeButton(android.R.string.no) {_, _ ->
//do noting
}
.show()
true
}
@@ -332,7 +329,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/**
* import config from qrcode
*/
fun importQRcode(forConfig: Boolean): Boolean {
private fun importQRcode(forConfig: Boolean): Boolean {
// try {
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
// .addCategory(Intent.CATEGORY_DEFAULT)
@@ -368,7 +365,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/**
* import config from clipboard
*/
fun importClipboard()
private fun importClipboard()
: Boolean {
try {
val clipboard = Utils.getClipboard(this)
@@ -380,27 +377,28 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
return true
}
fun importBatchConfig(server: String?, subid: String = "") {
val subid2 = if(subid.isNullOrEmpty()){
mainViewModel.subscriptionId
}else{
subid
}
val append = subid.isNullOrEmpty()
private fun importBatchConfig(server: String?) {
val dialog = AlertDialog.Builder(this)
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
.setCancelable(false)
.show()
var count = AngConfigManager.importBatchConfig(server, subid2, append)
if (count <= 0) {
count = AngConfigManager.importBatchConfig(Utils.decode(server!!), subid2, append)
}
lifecycleScope.launch(Dispatchers.IO) {
val count = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true)
delay(500L)
launch(Dispatchers.Main) {
if (count > 0) {
toast(R.string.toast_success)
mainViewModel.reloadServerList()
} else {
toast(R.string.toast_failure)
}
dialog.dismiss()
}
}
}
fun importConfigCustomClipboard()
private fun importConfigCustomClipboard()
: Boolean {
try {
val configText = Utils.getClipboard(this)
@@ -419,7 +417,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/**
* import config from local config file
*/
fun importConfigCustomLocal(): Boolean {
private fun importConfigCustomLocal(): Boolean {
try {
showFileChooser()
} catch (e: Exception) {
@@ -429,7 +427,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
return true
}
fun importConfigCustomUrlClipboard()
private fun importConfigCustomUrlClipboard()
: Boolean {
try {
val url = Utils.getClipboard(this)
@@ -447,7 +445,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/**
* import config from url
*/
fun importConfigCustomUrl(url: String?): Boolean {
private fun importConfigCustomUrl(url: String?): Boolean {
try {
if (!Utils.isValidUrl(url)) {
toast(R.string.toast_invalid_url)
@@ -474,43 +472,24 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/**
* import config from sub
*/
fun importConfigViaSub()
: Boolean {
try {
toast(R.string.title_sub_update)
MmkvManager.decodeSubscriptions().forEach {
if (TextUtils.isEmpty(it.first)
|| TextUtils.isEmpty(it.second.remarks)
|| TextUtils.isEmpty(it.second.url)
) {
return@forEach
}
if (!it.second.enabled) {
return@forEach
}
val url = Utils.idnToASCII(it.second.url)
if (!Utils.isValidUrl(url)) {
return@forEach
}
Log.d(ANG_PACKAGE, url)
private fun importConfigViaSub() : Boolean {
val dialog = AlertDialog.Builder(this)
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
.setCancelable(false)
.show()
lifecycleScope.launch(Dispatchers.IO) {
val configText = try {
Utils.getUrlContentWithCustomUserAgent(url)
} catch (e: Exception) {
e.printStackTrace()
val count = AngConfigManager.updateConfigViaSubAll()
delay(500L)
launch(Dispatchers.Main) {
toast("\"" + it.second.remarks + "\" " + getString(R.string.toast_failure))
if (count > 0) {
toast(R.string.toast_success)
mainViewModel.reloadServerList()
} else {
toast(R.string.toast_failure)
}
return@launch
dialog.dismiss()
}
launch(Dispatchers.Main) {
importBatchConfig(configText, it.first)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
@@ -541,8 +520,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
* read content from uri
*/
private fun readContentFromUri(uri: Uri) {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
RxPermissions(this)
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
.request(permission)
.subscribe {
if (it) {
try {
@@ -560,15 +544,18 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/**
* import customize config
*/
fun importCustomizeConfig(server: String?) {
private fun importCustomizeConfig(server: String?) {
try {
if (server == null || TextUtils.isEmpty(server)) {
toast(R.string.toast_none_data)
return
}
mainViewModel.appendCustomConfigServer(server)
if (mainViewModel.appendCustomConfigServer(server)) {
mainViewModel.reloadServerList()
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
//adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
} catch (e: Exception) {
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
@@ -577,7 +564,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
}
fun setTestState(content: String?) {
private fun setTestState(content: String?) {
binding.tvTestState.text = content
}
@@ -591,43 +578,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
// }
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) {
moveTaskToBack(false)
return true
}
return super.onKeyDown(keyCode, event)
}
fun showCircle() {
binding.fabProgressCircle.show()
}
fun hideCircle() {
try {
Observable.timer(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
try {
if (binding.fabProgressCircle.isShown) {
binding.fabProgressCircle.hide()
}
} catch (e: Exception) {
Log.w(ANG_PACKAGE, e)
}
}
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
}
}
override fun onBackPressed() {
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
binding.drawerLayout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
// Handle navigation view item clicks here.
when (item.itemId) {
@@ -642,15 +599,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
R.id.user_asset_setting -> {
startActivity(Intent(this, UserAssetActivity::class.java))
}
R.id.feedback -> {
Utils.openUri(this, AppConfig.v2rayNGIssues)
}
R.id.promotion -> {
Utils.openUri(this, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}")
Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
}
R.id.logcat -> {
startActivity(Intent(this, LogcatActivity::class.java))
}
R.id.about-> {
startActivity(Intent(this, AboutActivity::class.java))
}
}
binding.drawerLayout.closeDrawer(GravityCompat.START)
return true

View File

@@ -3,14 +3,15 @@ package com.v2ray.ang.ui
import android.content.Intent
import android.graphics.Color
import android.text.TextUtils
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AngApplication.Companion.application
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ItemQrcodeBinding
@@ -72,9 +73,9 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
}
if (guid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorSelected)
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent)
} else {
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorUnselected)
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
}
holder.itemMainBinding.tvSubscription.text = ""
val json = subStorage?.decodeString(config.subscriptionId)
@@ -96,7 +97,14 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
holder.itemMainBinding.tvType.text = config.configType.name.lowercase()
}
}
holder.itemMainBinding.tvStatistics.text = "${outbound?.getServerAddress()} : ${outbound?.getServerPort()}"
val strState = try{
"${outbound?.getServerAddress()?.dropLast(3)}*** : ${outbound?.getServerPort()}"
}catch(e: Exception){
""
}
holder.itemMainBinding.tvStatistics.text = strState
holder.itemMainBinding.layoutShare.setOnClickListener {
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
@@ -143,10 +151,15 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
.setPositiveButton(android.R.string.ok) { _, _ ->
removeServer(guid, position)
}
.setNegativeButton(android.R.string.no) {_, _ ->
//do noting
}
.show()
} else {
removeServer(guid, position)
}
} else {
application.toast(R.string.toast_action_not_allowed)
}
}
@@ -155,17 +168,15 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
if (guid != selected) {
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
if (!TextUtils.isEmpty(selected)) {
notifyItemChanged(mActivity.mainViewModel.getPosition(selected!!))
notifyItemChanged(mActivity.mainViewModel.getPosition(selected?:""))
}
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
if (isRunning) {
mActivity.showCircle()
Utils.stopVService(mActivity)
Observable.timer(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
V2RayServiceManager.startV2Ray(mActivity)
mActivity.hideCircle()
}
}
}
@@ -177,7 +188,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
} else {
holder.itemFooterBinding.layoutEdit.setOnClickListener {
Utils.openUri(mActivity, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}")
Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
}
}
}
@@ -228,7 +239,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder
class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
BaseViewHolder(itemFooterBinding.root), ItemTouchHelperViewHolder
BaseViewHolder(itemFooterBinding.root)
override fun onItemDismiss(position: Int) {
val guid = mActivity.mainViewModel.serversCache.getOrNull(position)?.guid ?: return

View File

@@ -8,9 +8,9 @@ import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R
@@ -19,21 +19,20 @@ import com.v2ray.ang.dto.AppInfo
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.v2RayApplication
import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import java.text.Collator
import java.util.*
class PerAppProxyActivity : BaseActivity() {
private lateinit var binding: ActivityBypassListBinding
private var adapter: PerAppProxyAdapter? = null
private var appsAll: List<AppInfo>? = null
private val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -41,19 +40,17 @@ class PerAppProxyActivity : BaseActivity() {
val view = binding.root
setContentView(view)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
binding.recyclerView.addItemDecoration(dividerItemDecoration)
val blacklist = defaultSharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, null)
val blacklist = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
AppManagerUtil.rxLoadNetworkAppList(this)
.subscribeOn(Schedulers.io())
.map {
if (blacklist != null) {
it.forEach { one ->
if ((blacklist.contains(one.packageName))) {
if (blacklist.contains(one.packageName)) {
one.isSelected = 1
} else {
one.isSelected = 0
@@ -137,14 +134,14 @@ class PerAppProxyActivity : BaseActivity() {
***/
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_PER_APP_PROXY, isChecked).apply()
settingsStorage.encode(AppConfig.PREF_PER_APP_PROXY, isChecked)
}
binding.switchPerAppProxy.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
binding.switchPerAppProxy.isChecked = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked ->
defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_BYPASS_APPS, isChecked).apply()
settingsStorage.encode(AppConfig.PREF_BYPASS_APPS, isChecked)
}
binding.switchBypassApps.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_BYPASS_APPS, false)
binding.switchBypassApps.isChecked = settingsStorage.getBoolean(AppConfig.PREF_BYPASS_APPS, false)
/***
et_search.setOnEditorActionListener { v, actionId, event ->
@@ -180,7 +177,7 @@ class PerAppProxyActivity : BaseActivity() {
override fun onPause() {
super.onPause()
adapter?.let {
defaultSharedPreferences.edit().putStringSet(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist).apply()
settingsStorage.encode(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist)
}
}
@@ -196,7 +193,7 @@ class PerAppProxyActivity : BaseActivity() {
}
override fun onQueryTextChange(newText: String?): Boolean {
filterProxyApp(newText!!)
filterProxyApp(newText?:"")
return false
}
})
@@ -212,12 +209,12 @@ class PerAppProxyActivity : BaseActivity() {
if (it.blacklist.containsAll(pkgNames)) {
it.apps.forEach {
val packageName = it.packageName
adapter?.blacklist!!.remove(packageName)
adapter?.blacklist?.remove(packageName)
}
} else {
it.apps.forEach {
val packageName = it.packageName
adapter?.blacklist!!.add(packageName)
adapter?.blacklist?.add(packageName)
}
}
it.notifyDataSetChanged()
@@ -281,7 +278,7 @@ class PerAppProxyActivity : BaseActivity() {
return false
}
adapter?.blacklist!!.clear()
adapter?.blacklist?.clear()
if (binding.switchBypassApps.isChecked) {
adapter?.let {
@@ -289,7 +286,7 @@ class PerAppProxyActivity : BaseActivity() {
val packageName = it.packageName
Log.d(ANG_PACKAGE, packageName)
if (!inProxyApps(proxyApps, packageName, force)) {
adapter?.blacklist!!.add(packageName)
adapter?.blacklist?.add(packageName)
println(packageName)
return@block
}
@@ -302,7 +299,7 @@ class PerAppProxyActivity : BaseActivity() {
val packageName = it.packageName
Log.d(ANG_PACKAGE, packageName)
if (inProxyApps(proxyApps, packageName, force)) {
adapter?.blacklist!!.add(packageName)
adapter?.blacklist?.add(packageName)
println(packageName)
return@block
}

View File

@@ -21,7 +21,6 @@ class RoutingSettingsActivity : BaseActivity() {
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))

View File

@@ -1,7 +1,7 @@
package com.v2ray.ang.ui
import android.Manifest
import android.app.Activity.RESULT_OK
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
@@ -11,14 +11,15 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.tbruyelle.rxpermissions.RxPermissions
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.FragmentRoutingSettingsBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.v2RayApplication
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class RoutingSettingsFragment : Fragment() {
@@ -27,7 +28,7 @@ class RoutingSettingsFragment : Fragment() {
private const val routing_arg = "routing_arg"
}
val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()) }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
@@ -47,8 +48,8 @@ class RoutingSettingsFragment : 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!!)
val content = settingsStorage?.getString(requireArguments().getString(routing_arg), "")
binding.etRoutingContent.text = Utils.getEditable(content)
setHasOptionsMenu(true)
}
@@ -84,7 +85,7 @@ class RoutingSettingsFragment : Fragment() {
private fun saveRouting() {
val content = binding.etRoutingContent.text.toString()
defaultSharedPreferences.edit().putString(requireArguments().getString(routing_arg), content).apply()
settingsStorage?.encode(requireArguments().getString(routing_arg), content)
activity?.toast(R.string.toast_success)
}
@@ -112,7 +113,7 @@ class RoutingSettingsFragment : Fragment() {
private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
val content = it.data?.getStringExtra("SCAN_RESULT")
binding.etRoutingContent.text = Utils.getEditable(content!!)
binding.etRoutingContent.text = Utils.getEditable(content)
}
}
@@ -128,7 +129,7 @@ class RoutingSettingsFragment : Fragment() {
var tag = ""
when (requireArguments().getString(routing_arg)) {
AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
tag = AppConfig.TAG_AGENT
tag = AppConfig.TAG_PROXY
}
AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
tag = AppConfig.TAG_DIRECT
@@ -144,7 +145,7 @@ class RoutingSettingsFragment : Fragment() {
val content = Utils.getUrlContext(url, 5000)
launch(Dispatchers.Main) {
val routingList = if (TextUtils.isEmpty(content)) {
Utils.readTextFromAssets(activity?.v2RayApplication!!, "custom_routing_$tag")
Utils.readTextFromAssets(activity?.v2RayApplication, "custom_routing_$tag")
} else {
content
}

View File

@@ -1,65 +1,60 @@
package com.v2ray.ang.ui
import android.Manifest
import android.app.Activity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.google.zxing.Result
import me.dm7.barcodescanner.zxing.ZXingScannerView
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Build
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import com.google.zxing.BarcodeFormat
import com.tbruyelle.rxpermissions.RxPermissions
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.QRCodeDecoder
import io.github.g00fy2.quickie.QRResult
import io.github.g00fy2.quickie.ScanCustomCode
import io.github.g00fy2.quickie.config.ScannerConfig
class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
class ScannerActivity : BaseActivity(){
private var mScannerView: ZXingScannerView? = null
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mScannerView = ZXingScannerView(this) // Programmatically initialize the scanner view
mScannerView?.setAutoFocus(true)
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)
if (settingsStorage?.decodeBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) {
launchScan()
}
}
public override fun onResume() {
super.onResume()
mScannerView!!.setResultHandler(this) // Register ourselves as a handler for scan results.
mScannerView!!.startCamera() // Start camera on resume
private fun launchScan(){
scanQrCode.launch(
ScannerConfig.build {
setHapticSuccessFeedback(true) // enable (default) or disable haptic feedback when a barcode was detected
setShowTorchToggle(true) // show or hide (default) torch/flashlight toggle button
setShowCloseButton(true) // show or hide (default) close button
}
)
}
public override fun onPause() {
super.onPause()
mScannerView!!.stopCamera() // Stop camera on pause
private fun handleResult(result: QRResult) {
if (result is QRResult.QRSuccess ) {
finished(result.content.rawValue?:"")
} 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) {
val intent = Intent()
intent.putExtra("SCAN_RESULT", text)
setResult(Activity.RESULT_OK, intent)
setResult(AppCompatActivity.RESULT_OK, intent)
finish()
}
@@ -69,9 +64,18 @@ class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.scan_code -> {
launchScan()
true
}
R.id.select_photo -> {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
RxPermissions(this)
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
.request(permission)
.subscribe {
if (it) {
try {
@@ -106,7 +110,7 @@ class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
try {
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
finished(text!!)
finished(text?:"")
} catch (e: Exception) {
e.printStackTrace()
toast(e.message.toString())

View File

@@ -5,26 +5,42 @@ import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
import com.v2ray.ang.R
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_PORT
import com.v2ray.ang.dto.V2rayConfig.Companion.TLS
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.ID_MAIN
import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.Utils.getIpv6Address
class ServerActivity : BaseActivity() {
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
private val settingsStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_SETTING,
MMKV.MULTI_PROCESS_MODE
)
}
private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() }
private val isRunning by lazy {
intent.getBooleanExtra("isRunning", false)
@@ -32,7 +48,8 @@ class ServerActivity : BaseActivity() {
&& editGuid == mainStorage?.decodeString(KEY_SELECTED_SERVER)
}
private val createConfigType by lazy {
EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value)) ?: EConfigType.VMESS
EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value))
?: EConfigType.VMESS
}
private val subscriptionId by lazy {
intent.getStringExtra("subscriptionId")
@@ -71,6 +88,7 @@ class ServerActivity : BaseActivity() {
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.
// 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.
@@ -84,14 +102,32 @@ class ServerActivity : BaseActivity() {
private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) }
private val sp_stream_security: Spinner? by lazy { findViewById(R.id.sp_stream_security) }
private val sp_allow_insecure: Spinner? by lazy { findViewById(R.id.sp_allow_insecure) }
private val container_allow_insecure: LinearLayout? by lazy { findViewById(R.id.l5) }
private val et_sni: EditText? by lazy { findViewById(R.id.et_sni) }
private val container_sni: LinearLayout? by lazy { findViewById(R.id.l2) }
private val sp_stream_fingerprint: Spinner? by lazy { findViewById(R.id.sp_stream_fingerprint) } //uTLS
private val container_fingerprint: LinearLayout? by lazy { findViewById(R.id.l3) }
private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) }
private val sp_header_type: Spinner? by lazy { findViewById(R.id.sp_header_type) }
private val sp_header_type_title: TextView? by lazy { findViewById(R.id.sp_header_type_title) }
private val tv_request_host: TextView? by lazy { findViewById(R.id.tv_request_host) }
private val et_request_host: EditText? by lazy { findViewById(R.id.et_request_host) }
private val tv_path: TextView? by lazy { findViewById(R.id.tv_path) }
private val et_path: EditText? by lazy { findViewById(R.id.et_path) }
private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
private val container_alpn: LinearLayout? by lazy { findViewById(R.id.l4) }
private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) }
private val container_public_key: LinearLayout? by lazy { findViewById(R.id.l6) }
private val et_short_id: EditText? by lazy { findViewById(R.id.et_short_id) }
private val container_short_id: LinearLayout? by lazy { findViewById(R.id.l7) }
private val et_spider_x: EditText? by lazy { findViewById(R.id.et_spider_x) }
private val container_spider_x: LinearLayout? by lazy { findViewById(R.id.l8) }
private val et_reserved1: EditText? by lazy { findViewById(R.id.et_reserved1) }
private val et_reserved2: EditText? by lazy { findViewById(R.id.et_reserved2) }
private val et_reserved3: EditText? by lazy { findViewById(R.id.et_reserved3) }
private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) }
private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -105,12 +141,19 @@ class ServerActivity : BaseActivity() {
EConfigType.SOCKS -> setContentView(R.layout.activity_server_socks)
EConfigType.VLESS -> setContentView(R.layout.activity_server_vless)
EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan)
EConfigType.WIREGUARD -> setContentView(R.layout.activity_server_wireguard)
}
sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
val types = transportTypes(networks[position])
sp_header_type?.isEnabled = types.size > 1
val adapter = ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
val adapter =
ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
sp_header_type?.adapter = adapter
sp_header_type_title?.text = if (networks[position] == "grpc")
@@ -121,70 +164,196 @@ class ServerActivity : BaseActivity() {
et_request_host?.text = Utils.getEditable(transportDetails[1])
et_path?.text = Utils.getEditable(transportDetails[2])
}
tv_request_host?.text = Utils.getEditable(
getString(
when (networks[position]) {
"tcp" -> R.string.server_lab_request_host_http
"ws" -> R.string.server_lab_request_host_ws
"httpupgrade" -> R.string.server_lab_request_host_httpupgrade
"splithttp" -> R.string.server_lab_request_host_splithttp
"h2" -> R.string.server_lab_request_host_h2
"quic" -> R.string.server_lab_request_host_quic
"grpc" -> R.string.server_lab_request_host_grpc
else -> R.string.server_lab_request_host
}
)
)
tv_path?.text = Utils.getEditable(
getString(
when (networks[position]) {
"kcp" -> R.string.server_lab_path_kcp
"ws" -> R.string.server_lab_path_ws
"httpupgrade" -> R.string.server_lab_path_httpupgrade
"splithttp" -> R.string.server_lab_path_splithttp
"h2" -> R.string.server_lab_path_h2
"quic" -> R.string.server_lab_path_quic
"grpc" -> R.string.server_lab_path_grpc
else -> R.string.server_lab_path
}
)
)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// do nothing
}
}
sp_stream_security?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
if (streamSecuritys[position].isBlank()) {
container_sni?.visibility = View.GONE
container_fingerprint?.visibility = View.GONE
container_alpn?.visibility = View.GONE
container_allow_insecure?.visibility = View.GONE
container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE
} else {
container_sni?.visibility = View.VISIBLE
container_fingerprint?.visibility = View.VISIBLE
container_alpn?.visibility = View.VISIBLE
if (streamSecuritys[position] == TLS) {
container_allow_insecure?.visibility = View.VISIBLE
container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE
} else {
container_allow_insecure?.visibility = View.GONE
container_alpn?.visibility = View.GONE
container_public_key?.visibility = View.VISIBLE
container_short_id?.visibility = View.VISIBLE
container_spider_x?.visibility = View.VISIBLE
}
}
}
override fun onNothingSelected(p0: AdapterView<*>?) {
// do nothing
}
}
if (config != null) {
bindingServer(config)
} else {
clearServer()
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
/**
* bingding seleced server config
* binding selected server config
*/
private fun bindingServer(config: ServerConfig): Boolean {
val outbound = config.getProxyOutbound() ?: return false
val streamSetting = config.outboundBean?.streamSettings ?: return false
et_remarks.text = Utils.getEditable(config.remarks)
et_address.text = Utils.getEditable(outbound.getServerAddress().orEmpty())
et_port.text = Utils.getEditable(outbound.getServerPort()?.toString() ?: DEFAULT_PORT.toString())
et_port.text =
Utils.getEditable(outbound.getServerPort()?.toString() ?: DEFAULT_PORT.toString())
et_id.text = Utils.getEditable(outbound.getPassword().orEmpty())
et_alterId?.text = Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString())
et_alterId?.text =
Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString())
if (config.configType == EConfigType.SOCKS) {
et_security?.text = Utils.getEditable(outbound.settings?.servers?.get(0)?.users?.get(0)?.user.orEmpty())
et_security?.text =
Utils.getEditable(outbound.settings?.servers?.get(0)?.users?.get(0)?.user.orEmpty())
} else if (config.configType == EConfigType.VLESS) {
et_security?.text = Utils.getEditable(outbound.getSecurityEncryption().orEmpty())
val flow = Utils.arrayFind(flows, outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow.orEmpty())
val flow = Utils.arrayFind(
flows,
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow.orEmpty()
)
if (flow >= 0) {
sp_flow?.setSelection(flow)
}
} else if (config.configType == EConfigType.TROJAN) {
val flow = Utils.arrayFind(flows, outbound.settings?.servers?.get(0)?.flow.orEmpty())
if (flow >= 0) {
sp_flow?.setSelection(flow)
} else if (config.configType == EConfigType.WIREGUARD) {
et_public_key?.text =
Utils.getEditable(outbound.settings?.peers?.get(0)?.publicKey.orEmpty())
if (outbound.settings?.reserved == null) {
et_reserved1?.text = Utils.getEditable("0")
et_reserved2?.text = Utils.getEditable("0")
et_reserved3?.text = Utils.getEditable("0")
} else {
et_reserved1?.text =
Utils.getEditable(outbound.settings?.reserved?.get(0).toString())
et_reserved2?.text =
Utils.getEditable(outbound.settings?.reserved?.get(1).toString())
et_reserved3?.text =
Utils.getEditable(outbound.settings?.reserved?.get(2).toString())
}
if (outbound.settings?.address == null) {
et_local_address?.text =
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
} else {
val list = outbound.settings?.address as List<*>
et_local_address?.text = Utils.getEditable(list.joinToString())
}
if (outbound.settings?.mtu == null) {
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
} else {
et_local_mtu?.text = Utils.getEditable(outbound.settings?.mtu.toString())
}
}
val securityEncryptions = if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
val security = Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty())
val securityEncryptions =
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
val security =
Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty())
if (security >= 0) {
sp_security?.setSelection(security)
}
val streamSetting = config.outboundBean?.streamSettings ?: return true
val streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security)
if (streamSecurity >= 0) {
sp_stream_security?.setSelection(streamSecurity)
(streamSetting.tlsSettings?: streamSetting.xtlsSettings)?.let { tlsSetting ->
val allowinsecure = Utils.arrayFind(allowinsecures, tlsSetting.allowInsecure.toString())
if (allowinsecure >= 0) {
sp_allow_insecure?.setSelection(allowinsecure)
}
(streamSetting.tlsSettings ?: streamSetting.realitySettings)?.let { tlsSetting ->
container_sni?.visibility = View.VISIBLE
container_fingerprint?.visibility = View.VISIBLE
container_alpn?.visibility = View.VISIBLE
et_sni?.text = Utils.getEditable(tlsSetting.serverName)
tlsSetting.fingerprint?.let {
val utlsIndex = Utils.arrayFind(uTlsItems, tlsSetting.fingerprint)
sp_stream_fingerprint?.setSelection(utlsIndex)
}
tlsSetting.alpn?.let {
val alpnIndex = Utils.arrayFind(alpns, Utils.removeWhiteSpace(tlsSetting.alpn.joinToString())!!)
val alpnIndex = Utils.arrayFind(
alpns,
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString())?:""
)
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)
@@ -215,6 +384,13 @@ class ServerActivity : BaseActivity() {
//et_security.text = null
sp_flow?.setSelection(0)
et_public_key?.text = null
et_reserved1?.text = Utils.getEditable("0")
et_reserved2?.text = Utils.getEditable("0")
et_reserved3?.text = Utils.getEditable("0")
et_local_address?.text =
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
return true
}
@@ -235,9 +411,14 @@ class ServerActivity : BaseActivity() {
toast(R.string.server_lab_port)
return false
}
val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(createConfigType)
val config =
MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(createConfigType)
if (config.configType != EConfigType.SOCKS && TextUtils.isEmpty(et_id.text.toString())) {
if (config.configType == EConfigType.TROJAN || config.configType == EConfigType.SHADOWSOCKS) {
toast(R.string.server_lab_id3)
} else {
toast(R.string.server_lab_id)
}
return false
}
sp_stream_security?.let {
@@ -261,11 +442,15 @@ class ServerActivity : BaseActivity() {
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
saveServers(server, port, config)
}
val wireguard = config.outboundBean?.settings
wireguard?.peers?.get(0)?.let { _ ->
savePeer(wireguard, port)
}
config.outboundBean?.streamSettings?.let {
saveStreamSettings(it)
}
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
config.subscriptionId = subscriptionId!!
config.subscriptionId = subscriptionId?:""
}
MmkvManager.encodeServerConfig(editGuid, config)
@@ -274,7 +459,11 @@ class ServerActivity : BaseActivity() {
return true
}
private fun saveVnext(vnext: V2rayConfig.OutboundBean.OutSettingsBean.VnextBean, port: Int, config: ServerConfig) {
private fun saveVnext(
vnext: V2rayConfig.OutboundBean.OutSettingsBean.VnextBean,
port: Int,
config: ServerConfig
) {
vnext.address = et_address.text.toString().trim()
vnext.port = port
vnext.users[0].id = et_id.text.toString().trim()
@@ -287,7 +476,11 @@ class ServerActivity : BaseActivity() {
}
}
private fun saveServers(server: V2rayConfig.OutboundBean.OutSettingsBean.ServersBean, port: Int, config: ServerConfig) {
private fun saveServers(
server: V2rayConfig.OutboundBean.OutSettingsBean.ServersBean,
port: Int,
config: ServerConfig
) {
server.address = et_address.text.toString().trim()
server.port = port
if (config.configType == EConfigType.SHADOWSOCKS) {
@@ -297,20 +490,32 @@ class ServerActivity : BaseActivity() {
if (TextUtils.isEmpty(et_security?.text) && TextUtils.isEmpty(et_id.text)) {
server.users = null
} else {
val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
val socksUsersBean =
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = et_security?.text.toString().trim()
socksUsersBean.pass = et_id.text.toString().trim()
server.users = listOf(socksUsersBean)
}
} else if (config.configType == EConfigType.TROJAN) {
server.password = et_id.text.toString().trim()
server.flow =
if (streamSecuritys[sp_stream_security?.selectedItemPosition ?: 0] == V2rayConfig.XTLS) {
flows[sp_flow?.selectedItemPosition ?: 0]
}
}
private fun savePeer(wireguard: V2rayConfig.OutboundBean.OutSettingsBean, port: Int) {
wireguard.secretKey = et_id.text.toString().trim()
wireguard.peers?.get(0)?.publicKey = et_public_key?.text.toString().trim()
wireguard.peers?.get(0)?.endpoint =
getIpv6Address(et_address.text.toString().trim()) + ":" + port
val reserved1 = Utils.parseInt(et_reserved1?.text.toString())
val reserved2 = Utils.parseInt(et_reserved2?.text.toString())
val reserved3 = Utils.parseInt(et_reserved3?.text.toString())
if (reserved1 > 0 || reserved2 > 0 || reserved3 > 0) {
wireguard.reserved = listOf(reserved1, reserved2, reserved3)
} else {
""
}
wireguard.reserved = null
}
wireguard.address = et_local_address?.text.toString().removeWhiteSpace().split(",")
wireguard.mtu = Utils.parseInt(et_local_mtu?.text.toString())
}
private fun saveStreamSettings(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean) {
@@ -321,8 +526,11 @@ class ServerActivity : BaseActivity() {
val sniField = et_sni?.text?.toString()?.trim() ?: return
val allowInsecureField = sp_allow_insecure?.selectedItemPosition ?: return
val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
var utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: return
var alpnIndex = sp_stream_alpn?.selectedItemPosition ?: return
val utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: return
val alpnIndex = sp_stream_alpn?.selectedItemPosition ?: return
val publicKey = et_public_key?.text?.toString()?.trim() ?: return
val shortId = et_short_id?.text?.toString()?.trim() ?: return
val spiderX = et_spider_x?.text?.toString()?.trim() ?: return
var sni = streamSetting.populateTransportSettings(
transport = networks[network],
@@ -333,7 +541,8 @@ class ServerActivity : BaseActivity() {
quicSecurity = requestHost,
key = path,
mode = transportTypes(networks[network])[type],
serviceName = path
serviceName = path,
authority = requestHost,
)
if (sniField.isNotBlank()) {
sni = sniField
@@ -344,38 +553,60 @@ class ServerActivity : BaseActivity() {
allowinsecures[allowInsecureField].toBoolean()
}
streamSetting.populateTlsSettings(streamSecuritys[streamSecurity], allowInsecure, sni, uTlsItems[utlsIndex], alpns[alpnIndex])
streamSetting.populateTlsSettings(
streamSecurity = streamSecuritys[streamSecurity],
allowInsecure = allowInsecure,
sni = sni,
fingerprint = uTlsItems[utlsIndex],
alpns = alpns[alpnIndex],
publicKey = publicKey,
shortId = shortId,
spiderX = spiderX
)
}
private fun transportTypes(network: String?): Array<out String> {
return if (network == "tcp") {
return when (network) {
"tcp" -> {
tcpTypes
} else if (network == "kcp" || network == "quic") {
}
"kcp", "quic" -> {
kcpAndQuicTypes
} else if (network == "grpc") {
}
"grpc" -> {
grpcModes
} else {
}
else -> {
arrayOf("---")
}
}
}
/**
* save server config
*/
private fun deleteServer(): Boolean {
if (editGuid.isNotEmpty()) {
if (editGuid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
if (editGuid != mainStorage?.decodeString(KEY_SELECTED_SERVER)) {
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
MmkvManager.removeServer(editGuid)
finish()
}
.setNegativeButton(android.R.string.no) { _, _ ->
// do nothing
}
.show()
} else {
MmkvManager.removeServer(editGuid)
finish()
}
} else {
application.toast(R.string.toast_action_not_allowed)
}
}
return true
@@ -403,10 +634,12 @@ class ServerActivity : BaseActivity() {
deleteServer()
true
}
R.id.save_config -> {
saveServer()
true
}
else -> super.onOptionsItemSelected(item)
}
}

View File

@@ -6,6 +6,7 @@ import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.blacksquircle.ui.editorkit.utils.EditorTheme
import com.blacksquircle.ui.language.json.JsonLanguage
import com.google.gson.*
import com.tencent.mmkv.MMKV
@@ -38,6 +39,9 @@ class ServerCustomConfigActivity : BaseActivity() {
setContentView(view)
title = getString(R.string.title_server)
if (!Utils.getDarkModeStatus(this)) {
binding.editor.colorScheme = EditorTheme.INTELLIJ_LIGHT
}
binding.editor.language = JsonLanguage()
val config = MmkvManager.decodeServerConfig(editGuid)
if (config != null) {
@@ -45,7 +49,6 @@ class ServerCustomConfigActivity : BaseActivity() {
} else {
clearServer()
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
/**
@@ -88,7 +91,7 @@ class ServerCustomConfigActivity : BaseActivity() {
}
val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM)
config.remarks = binding.etRemarks.text.toString().trim()
config.remarks = v2rayConfig.remarks ?: binding.etRemarks.text.toString().trim()
config.fullConfig = v2rayConfig
MmkvManager.encodeServerConfig(editGuid, config)
@@ -108,6 +111,9 @@ class ServerCustomConfigActivity : BaseActivity() {
MmkvManager.removeServer(editGuid)
finish()
}
.setNegativeButton(android.R.string.no) {_, _ ->
// do nothing
}
.show()
}
return true

View File

@@ -5,11 +5,23 @@ import android.os.Bundle
import android.text.TextUtils
import android.view.View
import androidx.activity.viewModels
import androidx.preference.*
import androidx.preference.CheckBoxPreference
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequest
import androidx.work.multiprocess.RemoteWorkManager
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.service.SubscriptionUpdater
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.SettingsViewModel
import java.util.concurrent.TimeUnit
class SettingsActivity : BaseActivity() {
private val settingsViewModel: SettingsViewModel by viewModels()
@@ -20,91 +32,118 @@ class SettingsActivity : BaseActivity() {
title = getString(R.string.title_settings)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
settingsViewModel.startListenPreferenceChange()
}
class SettingsFragment : PreferenceFragmentCompat() {
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
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 fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
// val autoRestart by lazy { findPreference(PREF_AUTO_RESTART) as CheckBoxPreference }
private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) }
private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_DNS) }
private val routingCustom by lazy { findPreference<Preference>(AppConfig.PREF_ROUTING_CUSTOM) }
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 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 remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) }
private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_DNS) }
private val delayTestUrl by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DELAY_TEST_URL) }
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
addPreferencesFromResource(R.xml.pref_settings)
routingCustom?.setOnPreferenceClickListener {
startActivity(Intent(activity, RoutingSettingsActivity::class.java))
false
}
// licenses.onClick {
// val fragment = LicensesDialogFragment.Builder(act)
// .setNotices(R.raw.licenses)
// .setIncludeOwnLicense(false)
// .build()
// fragment.show((act as AppCompatActivity).supportFragmentManager, null)
// }
//
// feedback.onClick {
// Utils.openUri(activity, "https://github.com/2dust/v2rayNG/issues")
// }
// tgGroup.onClick {
// // Utils.openUri(activity, "https://t.me/v2rayN")
// val intent = Intent(Intent.ACTION_VIEW, Uri.parse("tg:resolve?domain=v2rayN"))
// try {
// startActivity(intent)
// } catch (e: Exception) {
// e.printStackTrace()
// toast(R.string.toast_tg_app_not_found)
// }
// }
perAppProxy?.setOnPreferenceClickListener {
startActivity(Intent(activity, PerAppProxyActivity::class.java))
perAppProxy?.isChecked = true
false
}
remoteDns?.setOnPreferenceChangeListener { _, any ->
// remoteDns.summary = any as String
val nval = any as String
remoteDns?.summary = if (nval == "") AppConfig.DNS_AGENT else nval
true
}
domesticDns?.setOnPreferenceChangeListener { _, any ->
// domesticDns.summary = any as String
val nval = any as String
domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
true
}
localDns?.setOnPreferenceChangeListener { _, any ->
updateLocalDns(any as Boolean)
true
}
localDnsPort?.setOnPreferenceChangeListener { _, any ->
val nval = any as String
localDnsPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval
localDnsPort?.summary =
if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval
true
}
vpnDns?.setOnPreferenceChangeListener { _, any ->
vpnDns?.summary = any as String
true
}
routingCustom?.setOnPreferenceClickListener {
startActivity(Intent(activity, RoutingSettingsActivity::class.java))
false
}
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?.toLong()?.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.toLong() < 15) AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL else nval
autoUpdateInterval?.summary = nval
configureUpdateTask(nval.toLong())
true
}
socksPort?.setOnPreferenceChangeListener { _, any ->
val nval = any as String
socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval
@@ -115,57 +154,132 @@ class SettingsActivity : BaseActivity() {
httpPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_HTTP else nval
true
}
remoteDns?.setOnPreferenceChangeListener { _, any ->
val nval = any as String
remoteDns?.summary = if (nval == "") AppConfig.DNS_PROXY else nval
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 ->
updateMode(newValue.toString())
true
}
mode?.dialogLayoutResource = R.layout.preference_with_help_link
//loglevel.summary = "LogLevel"
}
override fun onStart() {
super.onStart()
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
updateMode(defaultSharedPreferences.getString(AppConfig.PREF_MODE, "VPN"))
var remoteDnsString = defaultSharedPreferences.getString(AppConfig.PREF_REMOTE_DNS, "")
domesticDns?.summary = defaultSharedPreferences.getString(AppConfig.PREF_DOMESTIC_DNS, "")
updateMode(settingsStorage.decodeString(AppConfig.PREF_MODE, "VPN"))
localDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
fakeDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FAKE_DNS_ENABLED, false)
localDnsPort?.summary = settingsStorage.decodeString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
vpnDns?.summary = settingsStorage.decodeString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
localDnsPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
socksPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
httpPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
updateMux(settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false))
mux?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false)
muxConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_CONCURRENCY, "8")
muxXudpConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
if (TextUtils.isEmpty(remoteDnsString)) {
remoteDnsString = AppConfig.DNS_AGENT
}
if (TextUtils.isEmpty(domesticDns?.summary)) {
domesticDns?.summary = AppConfig.DNS_DIRECT
}
remoteDns?.summary = remoteDnsString
vpnDns?.summary = defaultSharedPreferences.getString(AppConfig.PREF_VPN_DNS, remoteDnsString)
updateFragment(settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false))
fragment?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false)
fragmentPackets?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")
fragmentLength?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
fragmentInterval?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
if (TextUtils.isEmpty(localDnsPort?.summary)) {
localDnsPort?.summary = AppConfig.PORT_LOCAL_DNS
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()
}
if (TextUtils.isEmpty(socksPort?.summary)) {
socksPort?.summary = AppConfig.PORT_SOCKS
private fun initSharedPreference() {
listOf(
localDnsPort,
vpnDns,
muxConcurrency,
muxXudpConcurrency,
fragmentLength,
fragmentInterval,
autoUpdateInterval,
socksPort,
httpPort,
remoteDns,
domesticDns,
delayTestUrl
).forEach { key ->
key?.text = key?.summary.toString()
}
listOf(
AppConfig.PREF_SNIFFING_ENABLED,
).forEach { key ->
findPreference<CheckBoxPreference>(key)?.isChecked =
settingsStorage.decodeBool(key, true)
}
listOf(
AppConfig.PREF_ROUTE_ONLY_ENABLED,
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)
}
listOf(
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
AppConfig.PREF_ROUTING_MODE,
AppConfig.PREF_MUX_XUDP_QUIC,
AppConfig.PREF_FRAGMENT_PACKETS,
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)
}
if (TextUtils.isEmpty(httpPort?.summary)) {
httpPort?.summary = AppConfig.PORT_HTTP
}
}
private fun updateMode(mode: String?) {
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
val vpn = mode == "VPN"
perAppProxy?.isEnabled = vpn
perAppProxy?.isChecked = PreferenceManager.getDefaultSharedPreferences(requireActivity())
.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
perAppProxy?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
localDns?.isEnabled = vpn
fakeDns?.isEnabled = vpn
localDnsPort?.isEnabled = vpn
vpnDns?.isEnabled = vpn
if (vpn) {
updateLocalDns(defaultSharedPreferences.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false))
updateLocalDns(
settingsStorage.getBoolean(
AppConfig.PREF_LOCAL_DNS_ENABLED,
false
)
)
}
}
@@ -174,6 +288,77 @@ class SettingsActivity : BaseActivity() {
localDnsPort?.isEnabled = enabled
vpnDns?.isEnabled = !enabled
}
private fun configureUpdateTask(interval: Long) {
val rw = RemoteWorkManager.getInstance(AngApplication.application)
rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME)
rw.enqueueUniquePeriodicWork(
AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME,
ExistingPeriodicWorkPolicy.UPDATE,
PeriodicWorkRequest.Builder(
SubscriptionUpdater.UpdateTask::class.java,
interval,
TimeUnit.MINUTES
)
.apply {
setInitialDelay(interval, TimeUnit.MINUTES)
}
.build()
)
}
private fun cancelUpdateTask() {
val rw = RemoteWorkManager.getInstance(AngApplication.application)
rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME)
}
private fun updateMux(enabled: Boolean) {
muxConcurrency?.isEnabled = enabled
muxXudpConcurrency?.isEnabled = enabled
muxXudpQuic?.isEnabled = enabled
if (enabled) {
updateMuxConcurrency(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) {

View File

@@ -5,14 +5,22 @@ import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.multiprocess.RemoteWorkManager
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AngApplication
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubEditBinding
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.service.SubscriptionUpdater
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import java.util.concurrent.TimeUnit
class SubEditActivity : BaseActivity() {
private lateinit var binding: ActivitySubEditBinding
@@ -36,7 +44,6 @@ class SubEditActivity : BaseActivity() {
} else {
clearServer()
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
/**
@@ -46,6 +53,7 @@ class SubEditActivity : BaseActivity() {
binding.etRemarks.text = Utils.getEditable(subItem.remarks)
binding.etUrl.text = Utils.getEditable(subItem.url)
binding.chkEnable.isChecked = subItem.enabled
binding.autoUpdateCheck.isChecked = subItem.autoUpdate
return true
}
@@ -76,6 +84,7 @@ class SubEditActivity : BaseActivity() {
subItem.remarks = binding.etRemarks.text.toString()
subItem.url = binding.etUrl.text.toString()
subItem.enabled = binding.chkEnable.isChecked
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
if (TextUtils.isEmpty(subItem.remarks)) {
toast(R.string.sub_setting_remarks)
@@ -102,6 +111,9 @@ class SubEditActivity : BaseActivity() {
MmkvManager.removeSubscription(editSubId)
finish()
}
.setNegativeButton(android.R.string.no) {_, _ ->
// do nothing
}
.show()
}
return true
@@ -130,4 +142,5 @@ class SubEditActivity : BaseActivity() {
}
else -> super.onOptionsItemSelected(item)
}
}

View File

@@ -1,14 +1,22 @@
package com.v2ray.ang.ui
import android.content.Intent
import androidx.recyclerview.widget.LinearLayoutManager
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.v2ray.ang.R
import android.os.Bundle
import com.v2ray.ang.databinding.ActivitySubSettingBinding
import com.v2ray.ang.databinding.LayoutProgressBinding
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.MmkvManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SubSettingActivity : BaseActivity() {
private lateinit var binding: ActivitySubSettingBinding
@@ -27,8 +35,6 @@ class SubSettingActivity : BaseActivity() {
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onResume() {
@@ -39,9 +45,6 @@ class SubSettingActivity : BaseActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.action_sub_setting, menu)
menu.findItem(R.id.del_config)?.isVisible = false
menu.findItem(R.id.save_config)?.isVisible = false
return super.onCreateOptionsMenu(menu)
}
@@ -50,6 +53,30 @@ class SubSettingActivity : BaseActivity() {
startActivity(Intent(this, SubEditActivity::class.java))
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)
}
}

View File

@@ -2,20 +2,32 @@ package com.v2ray.ang.ui
import android.content.Intent
import android.graphics.Color
import android.text.TextUtils
import android.view.LayoutInflater
import androidx.recyclerview.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ItemQrcodeBinding
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.QRCodeDecoder
import com.v2ray.ang.util.Utils
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>() {
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) :
RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>() {
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 onBindViewHolder(holder: MainViewHolder, position: Int) {
@@ -24,14 +36,15 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
holder.itemSubSettingBinding.tvName.text = subItem.remarks
holder.itemSubSettingBinding.tvUrl.text = subItem.url
if (subItem.enabled) {
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorSelected)
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorAccent)
} else {
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorUnselected)
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(0)
}
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
mActivity.startActivity(Intent(mActivity, SubEditActivity::class.java)
mActivity.startActivity(
Intent(mActivity, SubEditActivity::class.java)
.putExtra("subId", subId)
)
}
@@ -40,11 +53,51 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
subStorage?.encode(subId, Gson().toJson(subItem))
notifyItemChanged(position)
}
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 {
return MainViewHolder(ItemRecyclerSubSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false))
return MainViewHolder(
ItemRecyclerSubSettingBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) : RecyclerView.ViewHolder(itemSubSettingBinding.root)
class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) :
RecyclerView.ViewHolder(itemSubSettingBinding.root)
}

View File

@@ -1,6 +1,6 @@
package com.v2ray.ang.ui
import android.app.Activity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.ArrayAdapter
@@ -11,7 +11,6 @@ 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
@@ -45,7 +44,7 @@ class TaskerActivity : BaseActivity() {
val adapter = ArrayAdapter(this,
android.R.layout.simple_list_item_single_choice, lstData)
listview = findViewById<View>(R.id.listview) as ListView
listview!!.adapter = adapter
listview?.adapter = adapter
init()
}
@@ -65,7 +64,7 @@ class TaskerActivity : BaseActivity() {
listview?.setItemChecked(pos, true)
}
}
} catch (e: WriterException) {
} catch (e: Exception) {
e.printStackTrace()
}
@@ -91,7 +90,7 @@ class TaskerActivity : BaseActivity() {
intent.putExtra(AppConfig.TASKER_EXTRA_BUNDLE, extraBundle)
intent.putExtra(AppConfig.TASKER_EXTRA_STRING_BLURB, blurb)
setResult(Activity.RESULT_OK, intent)
setResult(AppCompatActivity.RESULT_OK, intent)
finish()
}

View File

@@ -3,11 +3,12 @@ package com.v2ray.ang.ui
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import com.google.zxing.WriterException
import android.util.Log
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager
import java.net.URLDecoder
class UrlSchemeActivity : BaseActivity() {
private lateinit var binding: ActivityLogcatBinding
@@ -18,34 +19,57 @@ class UrlSchemeActivity : BaseActivity() {
val view = binding.root
setContentView(view)
var shareUrl: String = ""
try {
intent?.apply {
when (action) {
Intent.ACTION_SEND -> {
intent.apply {
if (action == Intent.ACTION_SEND) {
if ("text/plain" == type) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
shareUrl = it
parseUri(it)
}
}
}
Intent.ACTION_VIEW -> {
} else if (action == Intent.ACTION_VIEW) {
when (data?.host) {
"install-config" -> {
val uri: Uri? = intent.data
shareUrl = uri?.getQueryParameter("url")!!
val shareUrl = uri?.getQueryParameter("url") ?: ""
parseUri(shareUrl)
}
"install-sub" -> {
val uri: Uri? = intent.data
val shareUrl = uri?.getQueryParameter("url") ?: ""
parseUri(shareUrl)
}
}
toast(shareUrl)
val count = AngConfigManager.importBatchConfig(shareUrl, "", false)
if (count > 0) {
toast(R.string.toast_success)
} else {
else -> {
toast(R.string.toast_failure)
}
}
}
}
startActivity(Intent(this, MainActivity::class.java))
finish()
} catch (e: WriterException) {
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun parseUri(uriString: String?) {
if (uriString.isNullOrEmpty()) {
return
}
Log.d("UrlScheme", uriString)
val decodedUrl = URLDecoder.decode(uriString, "UTF-8")
val uri = Uri.parse(decodedUrl)
if (uri != null) {
val count = AngConfigManager.importBatchConfig(decodedUrl, "", false)
if (count > 0) {
toast(R.string.import_subscription_success)
} else {
toast(R.string.import_subscription_failure)
}
}
}
}

View File

@@ -4,6 +4,7 @@ import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.OpenableColumns
import android.util.Log
@@ -14,12 +15,14 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.gson.Gson
import com.tbruyelle.rxpermissions.RxPermissions
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubSettingBinding
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.toTrafficString
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager
@@ -38,9 +41,11 @@ import java.util.*
class UserAssetActivity : BaseActivity() {
private lateinit var binding: ActivitySubSettingBinding
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
val extDir by lazy { File(Utils.userAssetPath(this)) }
val geofiles = arrayOf("geosite.dat", "geoip.dat")
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -48,13 +53,17 @@ class UserAssetActivity : BaseActivity() {
val view = binding.root
setContentView(view)
title = getString(R.string.title_user_asset_setting)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = UserAssetAdapter()
}
override fun onResume() {
super.onResume()
binding.recyclerView.adapter?.notifyDataSetChanged()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_asset, menu)
return super.onCreateOptionsMenu(menu)
@@ -66,6 +75,11 @@ class UserAssetActivity : BaseActivity() {
true
}
R.id.add_url -> {
val intent = Intent(this, UserAssetUrlActivity::class.java)
startActivity(intent)
true
}
R.id.download_file -> {
downloadGeoFiles()
true
@@ -75,7 +89,14 @@ class UserAssetActivity : BaseActivity() {
}
private fun showFileChooser() {
RxPermissions(this).request(Manifest.permission.READ_EXTERNAL_STORAGE).subscribe {
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) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
@@ -91,18 +112,33 @@ class UserAssetActivity : BaseActivity() {
} catch (ex: android.content.ActivityNotFoundException) {
toast(R.string.toast_require_file_manager)
}
}
} else
toast(R.string.toast_permission_denied)
}
}
private val chooseFile =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { it ->
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
val assetId = Utils.getUuid()
try {
val assetItem = AssetUrlItem(
getCursorName(uri) ?: uri.toString(),
"file"
)
// check remarks unique
val assetList = MmkvManager.decodeAssetUrls()
if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
toast(R.string.msg_remark_is_duplicate)
return@registerForActivityResult
}
assetStorage?.encode(assetId, Gson().toJson(assetItem))
copyFile(uri)
} catch (e: Exception) {
toast(R.string.toast_asset_copy_failed)
MmkvManager.removeAssetUrl(assetId)
}
}
}
@@ -135,36 +171,45 @@ class UserAssetActivity : BaseActivity() {
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
toast(R.string.msg_downloading_content)
geofiles.forEach {
var assets = MmkvManager.decodeAssetUrls()
assets = addBuiltInGeoItems(assets)
assets.forEach {
//toast(getString(R.string.msg_downloading_content) + it)
lifecycleScope.launch(Dispatchers.IO) {
val result = downloadGeo(it, 60000, httpPort)
var result = downloadGeo(it.second, 60000, httpPort)
if (!result) {
result = downloadGeo(it.second, 60000, 0)
}
launch(Dispatchers.Main) {
if (result) {
toast(getString(R.string.toast_success) + " " + it)
toast(getString(R.string.toast_success) + " " + it.second.remarks)
binding.recyclerView.adapter?.notifyDataSetChanged()
} else {
toast(getString(R.string.toast_failure) + " " + it)
toast(getString(R.string.toast_failure) + " " + it.second.remarks)
}
}
}
}
}
private fun downloadGeo(name: String, timeout: Int, httpPort: Int): Boolean {
val url = AppConfig.geoUrl + name
val targetTemp = File(extDir, name + "_temp")
val target = File(extDir, name)
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
val targetTemp = File(extDir, item.remarks + "_temp")
val target = File(extDir, item.remarks)
var conn: HttpURLConnection? = null
//Log.d(AppConfig.ANG_PACKAGE, url)
try {
conn = URL(url).openConnection(
conn = if (httpPort == 0) {
URL(item.url).openConnection() as HttpURLConnection
} else {
URL(item.url).openConnection(
Proxy(
Proxy.Type.HTTP,
InetSocketAddress("127.0.0.1", httpPort)
)
) as HttpURLConnection
}
conn.connectTimeout = timeout
conn.readTimeout = timeout
val inputStream = conn.inputStream
@@ -184,33 +229,75 @@ class UserAssetActivity : BaseActivity() {
conn?.disconnect()
}
}
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>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
return UserAssetViewHolder(ItemRecyclerUserAssetBinding.inflate(LayoutInflater.from(parent.context), parent, false))
return UserAssetViewHolder(
ItemRecyclerUserAssetBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false)
)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
val file = extDir.listFiles()?.getOrNull(position) ?: return
holder.itemUserAssetBinding.assetName.text = file.name
var assets = MmkvManager.decodeAssetUrls();
assets = addBuiltInGeoItems(assets);
val item = assets.getOrNull(position) ?: return
// file with name == item.second.remarks
val file = extDir.listFiles()?.find { it.name == item.second.remarks }
holder.itemUserAssetBinding.assetName.text = item.second.remarks
if (file != null) {
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
holder.itemUserAssetBinding.assetProperties.text = "${file.length().toTrafficString()}${dateFormat.format(Date(file.lastModified()))}"
if (file.name in geofiles) {
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
} else {
holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE }
holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
}
holder.itemUserAssetBinding.layoutEdit.setOnClickListener {
val intent = Intent(this@UserAssetActivity, UserAssetUrlActivity::class.java)
intent.putExtra("assetId", item.first)
startActivity(intent)
}
holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
file.delete()
file?.delete()
MmkvManager.removeAssetUrl(item.first)
binding.recyclerView.adapter?.notifyItemRemoved(position)
}
}
override fun getItemCount(): Int {
return extDir.listFiles()?.size ?: 0
var assets = MmkvManager.decodeAssetUrls();
assets = addBuiltInGeoItems(assets);
return assets.size
}
}
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) : RecyclerView.ViewHolder(itemUserAssetBinding.root)
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
RecyclerView.ViewHolder(itemUserAssetBinding.root)
}

View File

@@ -0,0 +1,148 @@
package com.v2ray.ang.ui
import android.os.Bundle
import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
import com.v2ray.ang.R
import com.v2ray.ang.databinding.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 lateinit var binding: ActivityUserAssetUrlBinding
var del_config: MenuItem? = null
var save_config: MenuItem? = null
val extDir by lazy { File(Utils.userAssetPath(this)) }
private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityUserAssetUrlBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
title = getString(R.string.title_user_asset_add_url)
val json = assetStorage?.decodeString(editAssetId)
if (!json.isNullOrBlank()) {
bindingAsset(Gson().fromJson(json, AssetUrlItem::class.java))
} else {
clearAsset()
}
}
/**
* 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 {
val assetItem: AssetUrlItem
val json = assetStorage?.decodeString(editAssetId)
var assetId = editAssetId
if (!json.isNullOrBlank()) {
assetItem = Gson().fromJson(json, AssetUrlItem::class.java)
// 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
}
assetStorage?.encode(assetId, Gson().toJson(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)
}
}

View File

@@ -7,7 +7,6 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import com.v2ray.ang.dto.AppInfo
import rx.Observable
import java.util.*
object AppManagerUtil {
fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> {
@@ -22,7 +21,7 @@ object AppManagerUtil {
val appName = applicationInfo.loadLabel(packageManager).toString()
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)
apps.add(appInfo)
@@ -31,7 +30,8 @@ object AppManagerUtil {
return apps
}
fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> = Observable.unsafeCreate {
fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> =
Observable.unsafeCreate {
it.onNext(loadNetworkAppList(ctx))
}

View File

@@ -2,9 +2,11 @@ package com.v2ray.ang.util
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.dto.ServerAffiliationInfo
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.SubscriptionItem
import java.net.URI
object MmkvManager {
const val ID_MAIN = "MAIN"
@@ -12,6 +14,7 @@ object MmkvManager {
const val ID_SERVER_RAW = "SERVER_RAW"
const val ID_SERVER_AFF = "SERVER_AFF"
const val ID_SUB = "SUB"
const val ID_ASSET = "ASSET"
const val ID_SETTING = "SETTING"
const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
@@ -20,6 +23,7 @@ object MmkvManager {
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) }
private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
fun decodeServerList(): MutableList<String> {
val json = mainStorage?.decodeString(KEY_ANG_CONFIGS)
@@ -46,7 +50,7 @@ object MmkvManager {
serverStorage?.encode(key, Gson().toJson(config))
val serverList = decodeServerList()
if (!serverList.contains(key)) {
serverList.add(key)
serverList.add(0, key)
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
if (mainStorage?.decodeString(KEY_SELECTED_SERVER).isNullOrBlank()) {
mainStorage?.encode(KEY_SELECTED_SERVER, key)
@@ -102,8 +106,8 @@ object MmkvManager {
serverAffStorage?.encode(guid, Gson().toJson(aff))
}
fun clearAllTestDelayResults() {
serverAffStorage?.allKeys()?.forEach { key ->
fun clearAllTestDelayResults(keys: List<String>?) {
keys?.forEach { key ->
decodeServerAffiliationInfo(key)?.let { aff ->
aff.testDelayMillis = 0
serverAffStorage?.encode(key, Gson().toJson(aff))
@@ -118,8 +122,9 @@ object MmkvManager {
return 0
}
}
val uri = URI(Utils.fixIllegalUrl(url))
val subItem = SubscriptionItem()
subItem.remarks = "import sub"
subItem.remarks = Utils.urlDecode(uri.fragment ?: "import sub")
subItem.url = url
subStorage?.encode(Utils.getUuid(), Gson().toJson(subItem))
return 1
@@ -142,6 +147,22 @@ object MmkvManager {
removeServerViaSubid(subid)
}
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, Gson().fromJson(json, AssetUrlItem::class.java)))
}
}
assetUrlItems.sortedBy { (_, value) -> value.addedTime }
return assetUrlItems
}
fun removeAssetUrl(assetid: String) {
assetStorage?.remove(assetid)
}
fun removeAllServer() {
mainStorage?.clearAll()
serverStorage?.clearAll()
@@ -151,7 +172,7 @@ object MmkvManager {
fun removeInvalidServer() {
serverAffStorage?.allKeys()?.forEach { key ->
decodeServerAffiliationInfo(key)?.let { aff ->
if (aff.testDelayMillis <= 0L) {
if (aff.testDelayMillis < 0L) {
removeServer(key)
}
}

View File

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

View File

@@ -2,10 +2,16 @@ package com.v2ray.ang.util
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.google.zxing.*
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType
import com.google.zxing.EncodeHintType
import com.google.zxing.NotFoundException
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.common.GlobalHistogramBinarizer
import com.google.zxing.common.HybridBinarizer
import java.util.*
import com.google.zxing.qrcode.QRCodeReader
import com.google.zxing.qrcode.QRCodeWriter
import java.util.EnumMap
/**
* 描述:解析二维码图片
@@ -13,6 +19,40 @@ import java.util.*
object QRCodeDecoder {
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
}
}
/**
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
*
@@ -30,24 +70,37 @@ object QRCodeDecoder {
* @return 返回二维码图片里的内容 或 null
*/
fun syncDecodeQRCode(bitmap: Bitmap?): String? {
if (bitmap == null) {
return null
}
var source: RGBLuminanceSource? = null
try {
val width = bitmap!!.width
val width = bitmap.width
val height = bitmap.height
val pixels = IntArray(width * height)
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
source = RGBLuminanceSource(width, height, pixels)
return MultiFormatReader().decode(BinaryBitmap(HybridBinarizer(source)), HINTS).text
val qrReader = QRCodeReader()
try {
val result = try {
qrReader.decode(
BinaryBitmap(GlobalHistogramBinarizer(source)),
mapOf(DecodeHintType.TRY_HARDER to true)
)
} catch (e: NotFoundException) {
qrReader.decode(
BinaryBitmap(GlobalHistogramBinarizer(source.invert())),
mapOf(DecodeHintType.TRY_HARDER to true)
)
}
return result.text
} catch (e: Exception) {
e.printStackTrace()
}
if (source != null) {
try {
return MultiFormatReader().decode(BinaryBitmap(GlobalHistogramBinarizer(source)), HINTS).text
} catch (e2: Throwable) {
e2.printStackTrace()
}
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
@@ -76,23 +129,24 @@ object QRCodeDecoder {
init {
val allFormats: List<BarcodeFormat> = arrayListOf(
BarcodeFormat.AZTEC
,BarcodeFormat.CODABAR
,BarcodeFormat.CODE_39
,BarcodeFormat.CODE_93
,BarcodeFormat.CODE_128
,BarcodeFormat.DATA_MATRIX
,BarcodeFormat.EAN_8
,BarcodeFormat.EAN_13
,BarcodeFormat.ITF
,BarcodeFormat.MAXICODE
,BarcodeFormat.PDF_417
,BarcodeFormat.QR_CODE
,BarcodeFormat.RSS_14
,BarcodeFormat.RSS_EXPANDED
,BarcodeFormat.UPC_A
,BarcodeFormat.UPC_E
,BarcodeFormat.UPC_EAN_EXTENSION)
BarcodeFormat.AZTEC,
BarcodeFormat.CODABAR,
BarcodeFormat.CODE_39,
BarcodeFormat.CODE_93,
BarcodeFormat.CODE_128,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.EAN_8,
BarcodeFormat.EAN_13,
BarcodeFormat.ITF,
BarcodeFormat.MAXICODE,
BarcodeFormat.PDF_417,
BarcodeFormat.QR_CODE,
BarcodeFormat.RSS_14,
BarcodeFormat.RSS_EXPANDED,
BarcodeFormat.UPC_A,
BarcodeFormat.UPC_E,
BarcodeFormat.UPC_EAN_EXTENSION
)
HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE
HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats
HINTS[DecodeHintType.CHARACTER_SET] = "utf-8"

View File

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

View File

@@ -1,35 +1,32 @@
package com.v2ray.ang.util
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.text.Editable
import android.util.Base64
import com.google.zxing.WriterException
import android.graphics.Bitmap
import com.google.zxing.BarcodeFormat
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.EncodeHintType
import java.util.*
import kotlin.collections.HashMap
import android.content.ClipData
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.net.Uri
import android.os.Build
import android.os.LocaleList
import android.provider.Settings
import android.text.Editable
import android.util.Base64
import android.util.Log
import android.util.Patterns
import android.webkit.URLUtil
import androidx.appcompat.app.AppCompatDelegate
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.extension.toast
import java.net.*
import com.v2ray.ang.service.V2RayServiceManager
import java.io.IOException
import java.net.*
import java.util.*
object Utils {
@@ -42,8 +39,8 @@ object Utils {
* @param text
* @return
*/
fun getEditable(text: String): Editable {
return Editable.Factory.getInstance().newEditable(text)
fun getEditable(text: String?): Editable {
return Editable.Factory.getInstance().newEditable(text?:"")
}
/**
@@ -104,16 +101,16 @@ object Utils {
/**
* base64 decode
*/
fun decode(text: String): String {
fun decode(text: String?): String {
tryDecodeBase64(text)?.let { return it }
if (text.endsWith('=')) {
if (text?.endsWith('=')==true) {
// 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 {
return Base64.decode(text, Base64.NO_WRAP).toString(charset("UTF-8"))
} catch (e: Exception) {
@@ -143,18 +140,16 @@ object Utils {
* get remote dns servers from preference
*/
fun getRemoteDnsServers(): List<String> {
val remoteDns = settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_AGENT
val remoteDns = settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
if (ret.isEmpty()) {
return listOf(AppConfig.DNS_AGENT)
return listOf(AppConfig.DNS_PROXY)
}
return ret
}
fun getVpnDnsServers(): List<String> {
val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS)
?: settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS)
?: AppConfig.DNS_AGENT
val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS)?:AppConfig.DNS_VPN
return vpnDns.split(",").filter { isPureIpAddress(it) }
// allow empty, in that case dns will use system default
}
@@ -171,36 +166,6 @@ object Utils {
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
*/
@@ -213,7 +178,7 @@ object Utils {
//CIDR
if (addr.indexOf("/") > 0) {
val arr = addr.split("/")
if (arr.count() == 2 && Integer.parseInt(arr[1]) > 0) {
if (arr.count() == 2 && Integer.parseInt(arr[1]) > -1) {
addr = arr[0]
}
}
@@ -244,7 +209,7 @@ object Utils {
}
fun isPureIpAddress(value: String): Boolean {
return (isIpv4Address(value) || isIpv6Address(value))
return isIpv4Address(value) || isIpv6Address(value)
}
fun isIpv4Address(value: String): Boolean {
@@ -274,7 +239,7 @@ object Utils {
if (value != null && Patterns.WEB_URL.matcher(value).matches() || URLUtil.isValidUrl(value)) {
return true
}
} catch (e: WriterException) {
} catch (e: Exception) {
e.printStackTrace()
return false
}
@@ -317,7 +282,7 @@ object Utils {
fun urlDecode(url: String): String {
return try {
URLDecoder.decode(URLDecoder.decode(url), "utf-8")
URLDecoder.decode(url, "UTF-8")
} catch (e: Exception) {
e.printStackTrace()
url
@@ -337,7 +302,11 @@ object Utils {
/**
* 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 {
it.readText()
}
@@ -352,6 +321,19 @@ object Utils {
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(charset("UTF-8"))
return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
}
fun getUrlContext(url: String, timeout: Int): String {
var result: String
var conn: HttpURLConnection? = null
@@ -374,9 +356,20 @@ object Utils {
}
@Throws(IOException::class)
fun getUrlContentWithCustomUserAgent(urlStr: String?): String {
fun getUrlContentWithCustomUserAgent(urlStr: String?, timeout: Int = 30000, httpPort: Int = 0): String {
val url = URL(urlStr)
val conn = url.openConnection()
val conn = if (httpPort == 0) {
url.openConnection()
} else {
url.openConnection(
Proxy(
Proxy.Type.HTTP,
InetSocketAddress("127.0.0.1", httpPort)
)
)
}
conn.connectTimeout = timeout
conn.readTimeout = timeout
conn.setRequestProperty("Connection", "close")
conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
url.userInfo?.let {
@@ -391,11 +384,22 @@ object Utils {
fun getDarkModeStatus(context: Context): Boolean {
val mode = context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK
return mode == UI_MODE_NIGHT_YES
return mode != UI_MODE_NIGHT_NO
}
fun getIpv6Address(address: String): String {
return if (isIpv6Address(address)) {
fun setNightMode(context: Context) {
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)
} else {
address
@@ -435,5 +439,22 @@ object Utils {
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(): String {
val url = settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL)
return if (url.isNullOrEmpty()) AppConfig.DelayTestUrl else url
}
fun getDelayTestUrl(second: Boolean = false): String {
return if (second) {
AppConfig.DelayTestUrl2
} else {
val url = settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL)
if (url.isNullOrEmpty()) AppConfig.DelayTestUrl else url
}
}
}

View File

@@ -3,19 +3,34 @@ package com.v2ray.ang.util
import android.content.Context
import android.text.TextUtils
import android.util.Log
import com.google.gson.*
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.TAG_FRAGMENT
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ERoutingMode
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK
import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP
object V2rayConfigUtil {
private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
private val serverRawStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_SERVER_RAW,
MMKV.MULTI_PROCESS_MODE
)
}
private val settingsStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_SETTING,
MMKV.MULTI_PROCESS_MODE
)
}
data class Result(var status: Boolean, var content: String)
@@ -32,12 +47,18 @@ object V2rayConfigUtil {
} else {
raw
}
Log.d(ANG_PACKAGE, customConfig)
//Log.d(ANG_PACKAGE, customConfig)
return Result(true, customConfig)
}
val outbound = config.getProxyOutbound() ?: return Result(false, "")
val result = getV2rayNonCustomConfig(context, outbound)
Log.d(ANG_PACKAGE, result.content)
val address = outbound.getServerAddress() ?: return Result(false, "")
if (!Utils.isIpAddress(address) && !Utils.isValidUrl(address)) {
Log.d(ANG_PACKAGE, "$address is an invalid ip or domain")
return Result(false, "")
}
val result = getV2rayNonCustomConfig(context, outbound, config.remarks)
//Log.d(ANG_PACKAGE, result.content)
return result
} catch (e: Exception) {
e.printStackTrace()
@@ -48,7 +69,11 @@ object V2rayConfigUtil {
/**
* 生成v2ray的客户端配置文件
*/
private fun getV2rayNonCustomConfig(context: Context, outbound: V2rayConfig.OutboundBean): Result {
private fun getV2rayNonCustomConfig(
context: Context,
outbound: V2rayConfig.OutboundBean,
remarks: String,
): Result {
val result = Result(false, "")
//取得默认配置
val assets = Utils.readTextFromAssets(context, "v2ray_config.json")
@@ -64,10 +89,11 @@ object V2rayConfigUtil {
inbounds(v2rayConfig)
httpRequestObject(outbound)
updateOutboundWithGlobalSettings(outbound)
v2rayConfig.outbounds[0] = outbound
updateOutboundFragment(v2rayConfig)
routing(v2rayConfig)
fakedns(v2rayConfig)
@@ -81,6 +107,9 @@ object V2rayConfigUtil {
v2rayConfig.stats = null
v2rayConfig.policy = null
}
v2rayConfig.remarks = remarks
result.status = true
result.content = v2rayConfig.toPrettyPrinting()
return result
@@ -91,8 +120,14 @@ object V2rayConfigUtil {
*/
private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
try {
val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
val socksPort = Utils.parseInt(
settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT),
AppConfig.PORT_SOCKS.toInt()
)
val httpPort = Utils.parseInt(
settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT),
AppConfig.PORT_HTTP.toInt()
)
v2rayConfig.inbounds.forEach { curInbound ->
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) {
@@ -103,9 +138,12 @@ object V2rayConfigUtil {
v2rayConfig.inbounds[0].port = socksPort
val fakedns = settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED)
?: false
val sniffAllTlsAndHttp = settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true)
val sniffAllTlsAndHttp =
settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true)
?: true
v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp
v2rayConfig.inbounds[0].sniffing?.routeOnly =
settingsStorage?.decodeBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
if (!sniffAllTlsAndHttp) {
v2rayConfig.inbounds[0].sniffing?.destOverride?.clear()
}
@@ -129,9 +167,12 @@ object V2rayConfigUtil {
}
private fun fakedns(v2rayConfig: V2rayConfig) {
if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
&& settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
) {
v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean())
v2rayConfig.outbounds.filter { it.protocol == "freedom" }.forEach {
v2rayConfig.outbounds.filter { it.protocol == PROTOCOL_FREEDOM && it.tag == TAG_DIRECT }
.forEach {
it.settings?.domainStrategy = "UseIP"
}
}
@@ -142,47 +183,68 @@ object V2rayConfigUtil {
*/
private fun routing(v2rayConfig: V2rayConfig): Boolean {
try {
routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
?: "", AppConfig.TAG_AGENT, v2rayConfig)
routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
?: "", AppConfig.TAG_DIRECT, v2rayConfig)
routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
?: "", AppConfig.TAG_BLOCKED, v2rayConfig)
routingUserRule(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
?: "", AppConfig.TAG_PROXY, v2rayConfig
)
routingUserRule(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
?: "", 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)
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
// v2rayConfig.routing.domainMatcher = "mph"
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
// Hardcode googleapis.cn
// Hardcode googleapis.cn gstatic.com
val googleapisRoute = V2rayConfig.RoutingBean.RulesBean(
type = "field",
outboundTag = AppConfig.TAG_AGENT,
domain = arrayListOf("domain:googleapis.cn")
outboundTag = AppConfig.TAG_PROXY,
domain = arrayListOf("domain:googleapis.cn", "domain:gstatic.com")
)
when (routingMode) {
ERoutingMode.BYPASS_LAN.value -> {
routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig)
routingGeo("ip", "private", TAG_DIRECT, v2rayConfig)
}
ERoutingMode.BYPASS_MAINLAND.value -> {
routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig)
routingGeo("", "cn", TAG_DIRECT, v2rayConfig)
routingGeo("domain", "geolocation-cn", 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)
routingGeo("ip", "private", TAG_DIRECT, v2rayConfig)
routingGeo("", "cn", TAG_DIRECT, v2rayConfig)
routingGeo("domain", "geolocation-cn", TAG_DIRECT, v2rayConfig)
v2rayConfig.routing.rules.add(0, googleapisRoute)
}
ERoutingMode.GLOBAL_DIRECT.value -> {
val globalDirect = V2rayConfig.RoutingBean.RulesBean(
type = "field",
outboundTag = AppConfig.TAG_DIRECT,
outboundTag = TAG_DIRECT,
port = "0-65535"
)
v2rayConfig.routing.rules.add(globalDirect)
}
}
if (routingMode != ERoutingMode.GLOBAL_DIRECT.value) {
v2rayConfig.routing.rules.add(
V2rayConfig.RoutingBean.RulesBean(
outboundTag = AppConfig.TAG_PROXY,
port = "0-65535"
)
)
}
} catch (e: Exception) {
e.printStackTrace()
return false
@@ -190,13 +252,17 @@ object V2rayConfigUtil {
return true
}
private fun routingGeo(ipOrDomain: String, code: String, tag: String, v2rayConfig: V2rayConfig) {
private fun routingGeo(
ipOrDomain: String,
code: String,
tag: String,
v2rayConfig: V2rayConfig
) {
try {
if (!TextUtils.isEmpty(code)) {
//IP
if (ipOrDomain == "ip" || ipOrDomain == "") {
val rulesIP = V2rayConfig.RoutingBean.RulesBean()
rulesIP.type = "field"
rulesIP.outboundTag = tag
rulesIP.ip = ArrayList()
rulesIP.ip?.add("geoip:$code")
@@ -206,7 +272,6 @@ object V2rayConfigUtil {
if (ipOrDomain == "domain" || ipOrDomain == "") {
//Domain
val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
rulesDomain.type = "field"
rulesDomain.outboundTag = tag
rulesDomain.domain = ArrayList()
rulesDomain.domain?.add("geosite:$code")
@@ -223,33 +288,27 @@ object V2rayConfigUtil {
if (!TextUtils.isEmpty(userRule)) {
//Domain
val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
rulesDomain.type = "field"
rulesDomain.outboundTag = tag
rulesDomain.domain = ArrayList()
//IP
val rulesIP = V2rayConfig.RoutingBean.RulesBean()
rulesIP.type = "field"
rulesIP.outboundTag = tag
rulesIP.ip = ArrayList()
userRule.split(",").map { it.trim() }.forEach {
if (Utils.isIpAddress(it) || it.startsWith("geoip:")) {
if (it.startsWith("ext:") && it.contains("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:"))
{
} else if (Utils.isIpAddress(it) || it.startsWith("geoip:")) {
rulesIP.ip?.add(it)
} else if (it.isNotEmpty()) {
rulesDomain.domain?.add(it)
}
}
if (rulesDomain.domain?.size!! > 0) {
if ((rulesDomain.domain?.size ?: 0) > 0) {
v2rayConfig.routing.rules.add(rulesDomain)
}
if (rulesIP.ip?.size!! > 0) {
if ((rulesIP.ip?.size ?: 0) > 0) {
v2rayConfig.routing.rules.add(rulesIP)
}
}
@@ -275,24 +334,37 @@ object V2rayConfigUtil {
try {
if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
val geositeCn = arrayListOf("geosite:cn")
val proxyDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
?: "")
val directDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
?: "")
val proxyDomain = userRule2Domian(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
?: ""
)
val directDomain = userRule2Domian(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
?: ""
)
// fakedns with all domains to make it always top priority
v2rayConfig.dns.servers?.add(0,
V2rayConfig.DnsBean.ServersBean(address = "fakedns", domains = geositeCn.plus(proxyDomain).plus(directDomain)))
v2rayConfig.dns.servers?.add(
0,
V2rayConfig.DnsBean.ServersBean(
address = "fakedns",
domains = geositeCn.plus(proxyDomain).plus(directDomain)
)
)
}
// DNS inbound对象
val remoteDns = Utils.getRemoteDnsServers()
if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) {
val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean(
address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else "1.1.1.1",
address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else AppConfig.DNS_PROXY,
port = 53,
network = "tcp,udp")
network = "tcp,udp"
)
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()
)
v2rayConfig.inbounds.add(
V2rayConfig.InboundBean(
tag = "dns-in",
@@ -300,7 +372,9 @@ object V2rayConfigUtil {
listen = "127.0.0.1",
protocol = "dokodemo-door",
settings = dnsInboundSettings,
sniffing = null))
sniffing = null
)
)
}
// DNS outbound对象
@@ -311,15 +385,18 @@ object V2rayConfigUtil {
tag = "dns-out",
settings = null,
streamSettings = null,
mux = null))
mux = null
)
)
}
// DNS routing tag
v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean(
type = "field",
v2rayConfig.routing.rules.add(
0, V2rayConfig.RoutingBean.RulesBean(
inboundTag = arrayListOf("dns-in"),
outboundTag = "dns-out",
domain = null)
domain = null
)
)
} catch (e: Exception) {
e.printStackTrace()
@@ -332,44 +409,77 @@ object V2rayConfigUtil {
try {
val hosts = mutableMapOf<String, String>()
val servers = ArrayList<Any>()
val remoteDns = Utils.getRemoteDnsServers()
val proxyDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
?: "")
//remote Dns
val remoteDns = Utils.getRemoteDnsServers()
val proxyDomain = userRule2Domian(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
?: ""
)
remoteDns.forEach {
servers.add(it)
}
if (proxyDomain.size > 0) {
servers.add(V2rayConfig.DnsBean.ServersBean(remoteDns.first(), 53, proxyDomain, null))
servers.add(
V2rayConfig.DnsBean.ServersBean(
remoteDns.first(),
53,
proxyDomain,
null
)
)
}
// domestic DNS
val directDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
?: "")
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value
if (directDomain.size > 0 || routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
val domesticDns = Utils.getDomesticDnsServers()
val geositeCn = arrayListOf("geosite:cn")
val directDomain = userRule2Domian(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
?: ""
)
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
val isCnRoutingMode =
(routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value)
val geoipCn = arrayListOf("geoip:cn")
if (directDomain.size > 0) {
servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, directDomain, geoipCn))
}
if (routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, geositeCn, geoipCn))
}
if (Utils.isPureIpAddress(domesticDns.first())) {
v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean(
type = "field",
outboundTag = AppConfig.TAG_DIRECT,
port = "53",
ip = arrayListOf(domesticDns.first()),
domain = null)
servers.add(
V2rayConfig.DnsBean.ServersBean(
domesticDns.first(),
53,
directDomain,
if (isCnRoutingMode) geoipCn else null
)
)
}
if (isCnRoutingMode) {
val geositeCn = arrayListOf("geosite:cn", "geosite:geolocation-cn")
servers.add(
V2rayConfig.DnsBean.ServersBean(
domesticDns.first(),
53,
geositeCn,
geoipCn
)
)
}
val blkDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
?: "")
if (Utils.isPureIpAddress(domesticDns.first())) {
v2rayConfig.routing.rules.add(
0, V2rayConfig.RoutingBean.RulesBean(
outboundTag = TAG_DIRECT,
port = "53",
ip = arrayListOf(domesticDns.first()),
domain = null
)
)
}
//block dns
val blkDomain = userRule2Domian(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
?: ""
)
if (blkDomain.size > 0) {
hosts.putAll(blkDomain.map { it to "127.0.0.1" })
}
@@ -380,16 +490,18 @@ object V2rayConfigUtil {
// DNS dns对象
v2rayConfig.dns = V2rayConfig.DnsBean(
servers = servers,
hosts = hosts)
hosts = hosts
)
// DNS routing
if (Utils.isPureIpAddress(remoteDns.first())) {
v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean(
type = "field",
outboundTag = AppConfig.TAG_AGENT,
v2rayConfig.routing.rules.add(
0, V2rayConfig.RoutingBean.RulesBean(
outboundTag = AppConfig.TAG_PROXY,
port = "53",
ip = arrayListOf(remoteDns.first()),
domain = null)
domain = null
)
)
}
} catch (e: Exception) {
@@ -399,10 +511,49 @@ object V2rayConfigUtil {
return true
}
private fun httpRequestObject(outbound: V2rayConfig.OutboundBean): Boolean {
private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean {
try {
var muxEnabled = settingsStorage?.decodeBool(AppConfig.PREF_MUX_ENABLED, false)
val protocol = outbound.protocol
if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.SOCKS.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true)
|| protocol.equals(EConfigType.WIREGUARD.name, true)
) {
muxEnabled = false
} else if (protocol.equals(EConfigType.VLESS.name, true)
&& outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.isNotEmpty() == true
) {
muxEnabled = false
}
if (muxEnabled == true) {
outbound.mux?.enabled = true
outbound.mux?.concurrency =
settingsStorage?.decodeInt(AppConfig.PREF_MUX_CONCURRENCY) ?: 8
outbound.mux?.xudpConcurrency =
settingsStorage?.decodeInt(AppConfig.PREF_MUX_XUDP_CONCURRENCY) ?: 8
outbound.mux?.xudpProxyUDP443 =
settingsStorage?.decodeString(AppConfig.PREF_MUX_XUDP_QUIC) ?: "reject"
} else {
outbound.mux?.enabled = false
outbound.mux?.concurrency = -1
}
if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
var localTunAddr = if (outbound.settings?.address == null) {
listOf(WIREGUARD_LOCAL_ADDRESS_V4, WIREGUARD_LOCAL_ADDRESS_V6)
} else {
outbound.settings?.address as List<*>
}
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) != true) {
localTunAddr = listOf(localTunAddr.first())
}
outbound.settings?.address = localTunAddr
}
if (outbound.streamSettings?.network == DEFAULT_NETWORK
&& outbound.streamSettings?.tcpSettings?.header?.type == HTTP) {
&& outbound.streamSettings?.tcpSettings?.header?.type == HTTP
) {
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
@@ -419,9 +570,10 @@ object V2rayConfigUtil {
} else {
path
}
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host!!
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host
}
} catch (e: Exception) {
e.printStackTrace()
return false
@@ -429,4 +581,62 @@ object V2rayConfigUtil {
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"
)
)
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
}
}

View File

@@ -0,0 +1,102 @@
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()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,73 @@
package com.v2ray.ang.util.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.util.Utils
import java.net.URI
object WireguardFmt {
fun parseWireguard(str: String): ServerConfig? {
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery != null) {
val config = ServerConfig.create(EConfigType.WIREGUARD)
config.remarks = Utils.urlDecode(uri.fragment ?: "")
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"] ?: ""
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 toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
dicQuery["publickey"] =
Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString())
if (outbound.settings?.reserved != null) {
dicQuery["reserved"] = Utils.urlEncode(
Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString())
.toString()
)
}
dicQuery["address"] = Utils.urlEncode(
Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString())
.toString()
)
if (outbound.settings?.mtu != null) {
dicQuery["mtu"] = outbound.settings?.mtu.toString()
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
Utils.urlEncode(outbound.getPassword().toString()),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + query + remark
}
}

View File

@@ -1,7 +1,13 @@
package com.v2ray.ang.viewmodel
import android.app.Application
import android.content.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.IntentFilter
import android.content.res.AssetManager
import android.os.Build
import android.util.Log
import android.view.LayoutInflater
import android.widget.ArrayAdapter
@@ -16,20 +22,49 @@ import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R
import com.v2ray.ang.databinding.DialogConfigFilterBinding
import com.v2ray.ang.dto.*
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.*
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.KEY_ANG_CONFIGS
import kotlinx.coroutines.*
import java.util.*
import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.V2rayConfigUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.util.Collections
class MainViewModel(application: Application) : AndroidViewModel(application) {
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 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
)
}
var serverList = MmkvManager.decodeServerList()
var subscriptionId: String = ""
var keywordFilter: String = ""
var subscriptionId: String = settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "")?:""
var keywordFilter: String = settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
private set
val serversCache = mutableListOf<ServersCache>()
val isRunning by lazy { MutableLiveData<Boolean>() }
@@ -40,7 +75,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun startListenBroadcast() {
isRunning.value = false
getApplication<AngApplication>().registerReceiver(mMsgReceiver, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
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, "")
}
@@ -67,15 +113,26 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
}
fun appendCustomConfigServer(server: String) {
fun appendCustomConfigServer(server: String): Boolean {
if (server.contains("inbounds")
&& server.contains("outbounds")
&& server.contains("routing")
) {
try {
val config = ServerConfig.create(EConfigType.CUSTOM)
config.remarks = System.currentTimeMillis().toString()
config.subscriptionId = subscriptionId
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
val key = MmkvManager.encodeServerConfig("", config)
serverRawStorage?.encode(key, server)
serverList.add(key)
serversCache.add(ServersCache(key,config))
serverList.add(0, key)
serversCache.add(0, ServersCache(key, config))
return true
} catch (e: Exception) {
e.printStackTrace()
}
}
return false
}
fun swapServer(fromPosition: Int, toPosition: Int) {
@@ -102,7 +159,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun testAllTcping() {
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
SpeedtestUtil.closeAllTcpSockets()
MmkvManager.clearAllTestDelayResults()
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
updateListAction.value = -1 // update all
getApplication<AngApplication>().toast(R.string.connection_test_testing)
@@ -125,15 +182,21 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun testAllRealPing() {
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "")
MmkvManager.clearAllTestDelayResults()
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
updateListAction.value = -1 // update all
val serversCopy = serversCache.toList() // Create a copy of the list
getApplication<AngApplication>().toast(R.string.connection_test_testing)
viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread
for (item in serversCache) {
for (item in serversCopy) {
val config = V2rayConfigUtil.getV2rayConfig(getApplication(), item.guid)
if (config.status) {
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, Pair(item.guid, config.content))
MessageUtil.sendMsg2TestService(
getApplication(),
AppConfig.MSG_MEASURE_CONFIG,
Pair(item.guid, config.content)
)
}
}
}
@@ -155,7 +218,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
val ivBinding = DialogConfigFilterBinding.inflate(LayoutInflater.from(context))
ivBinding.spSubscriptionId.adapter = ArrayAdapter<String>( context, android.R.layout.simple_spinner_dropdown_item, listRemarks)
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)
@@ -169,6 +236,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
subscriptions[position].first
}
keywordFilter = ivBinding.etKeyword.text.toString()
settingsStorage?.encode(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId)
settingsStorage?.encode(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
reloadServerList()
dialogInterface?.dismiss()
@@ -201,29 +270,85 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return -1
}
fun removeDuplicateServer() {
val deleteServer = mutableListOf<String>()
serversCache.forEachIndexed { index, it ->
val outbound = it.config.getProxyOutbound()
serversCache.forEachIndexed { index2, it2 ->
if (index2 > index) {
val outbound2 = it2.config.getProxyOutbound()
if (outbound == outbound2 && !deleteServer.contains(it2.guid)) {
deleteServer.add(it2.guid)
}
}
}
}
for (it in deleteServer) {
MmkvManager.removeServer(it)
}
getApplication<AngApplication>().toast(
getApplication<AngApplication>().getString(
R.string.title_del_duplicate_config_count,
deleteServer.count()
)
)
}
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)
}
}
}
private val mMsgReceiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context?, intent: Intent?) {
when (intent?.getIntExtra("key", 0)) {
AppConfig.MSG_STATE_RUNNING -> {
isRunning.value = true
}
AppConfig.MSG_STATE_NOT_RUNNING -> {
isRunning.value = false
}
AppConfig.MSG_STATE_START_SUCCESS -> {
getApplication<AngApplication>().toast(R.string.toast_services_success)
isRunning.value = true
}
AppConfig.MSG_STATE_START_FAILURE -> {
getApplication<AngApplication>().toast(R.string.toast_services_failure)
isRunning.value = false
}
AppConfig.MSG_STATE_STOP_SUCCESS -> {
isRunning.value = false
}
AppConfig.MSG_MEASURE_DELAY_SUCCESS -> {
updateTestResultAction.value = intent.getStringExtra("content")
}
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
val resultPair = intent.getSerializableExtra("content") as Pair<String, Long>
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)

View File

@@ -8,17 +8,26 @@ import androidx.preference.PreferenceManager
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
class SettingsViewModel(application: Application) : AndroidViewModel(application), SharedPreferences.OnSharedPreferenceChangeListener {
class SettingsViewModel(application: Application) : AndroidViewModel(application),
SharedPreferences.OnSharedPreferenceChangeListener {
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
private val settingsStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_SETTING,
MMKV.MULTI_PROCESS_MODE
)
}
fun startListenPreferenceChange() {
PreferenceManager.getDefaultSharedPreferences(getApplication()).registerOnSharedPreferenceChangeListener(this)
PreferenceManager.getDefaultSharedPreferences(getApplication())
.registerOnSharedPreferenceChangeListener(this)
}
override fun onCleared() {
PreferenceManager.getDefaultSharedPreferences(getApplication()).unregisterOnSharedPreferenceChangeListener(this)
PreferenceManager.getDefaultSharedPreferences(getApplication())
.unregisterOnSharedPreferenceChangeListener(this)
Log.i(AppConfig.ANG_PACKAGE, "Settings ViewModel is cleared")
super.onCleared()
}
@@ -30,18 +39,28 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
AppConfig.PREF_VPN_DNS,
AppConfig.PREF_REMOTE_DNS,
AppConfig.PREF_DOMESTIC_DNS,
AppConfig.PREF_DELAY_TEST_URL,
AppConfig.PREF_LOCAL_DNS_PORT,
AppConfig.PREF_SOCKS_PORT,
AppConfig.PREF_HTTP_PORT,
AppConfig.PREF_LOGLEVEL,
AppConfig.PREF_LANGUAGE,
AppConfig.PREF_UI_MODE_NIGHT,
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
AppConfig.PREF_ROUTING_MODE,
AppConfig.PREF_V2RAY_ROUTING_AGENT,
AppConfig.PREF_V2RAY_ROUTING_BLOCKED,
AppConfig.PREF_V2RAY_ROUTING_DIRECT, -> {
AppConfig.PREF_V2RAY_ROUTING_DIRECT,
AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL,
AppConfig.PREF_FRAGMENT_PACKETS,
AppConfig.PREF_FRAGMENT_LENGTH,
AppConfig.PREF_FRAGMENT_INTERVAL,
AppConfig.PREF_MUX_XUDP_QUIC,
-> {
settingsStorage?.encode(key, sharedPreferences.getString(key, ""))
}
AppConfig.PREF_ROUTE_ONLY_ENABLED,
AppConfig.PREF_SPEED_ENABLED,
AppConfig.PREF_PROXY_SHARING,
AppConfig.PREF_LOCAL_DNS_ENABLED,
@@ -50,15 +69,30 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
AppConfig.PREF_PREFER_IPV6,
AppConfig.PREF_PER_APP_PROXY,
AppConfig.PREF_BYPASS_APPS,
AppConfig.PREF_CONFIRM_REMOVE, -> {
AppConfig.PREF_CONFIRM_REMOVE,
AppConfig.PREF_START_SCAN_IMMEDIATE,
AppConfig.SUBSCRIPTION_AUTO_UPDATE,
AppConfig.PREF_FRAGMENT_ENABLED,
AppConfig.PREF_MUX_ENABLED,
-> {
settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false))
}
AppConfig.PREF_SNIFFING_ENABLED -> {
settingsStorage?.encode(key, sharedPreferences.getBoolean(key, true))
}
AppConfig.PREF_PER_APP_PROXY_SET -> {
settingsStorage?.encode(key, sharedPreferences.getStringSet(key, setOf()))
}
AppConfig.PREF_MUX_CONCURRENCY,
AppConfig.PREF_MUX_XUDP_CONCURRENCY -> {
settingsStorage?.encode(key, sharedPreferences.getString(key, "8"))
}
// AppConfig.PREF_PER_APP_PROXY_SET -> {
// settingsStorage?.encode(key, sharedPreferences.getStringSet(key, setOf()))
// }
}
if (key == AppConfig.PREF_UI_MODE_NIGHT) {
Utils.setNightMode(getApplication())
}
}
}

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M810.7,206.3A426.7,426.7 0,1 0,512 938.7h5.5A426.7,426.7 0,0 0,810.7 206.3zM771.8,765A360.1,360.1 0,0 1,517.1 874.7L512,874.7a362.7,362.7 0,0 1,-4.9 -725.3h3.4a362.7,362.7 0,0 1,261.3 615.7z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M547.2,390a57.4,57.4 0,0 0,62.3 -55.3,39.3 39.3,0 0,0 -45,-42.7 58.9,58.9 0,0 0,-64 54.6c-1.7,26.9 13.9,43.3 46.7,43.3zM548.3,663.5c-5.5,0 -7.9,-7.3 -2.3,-28.2l31.1,-118c11.7,-42.7 7.9,-71.3 -15.8,-71.3 -28.4,0 -94.7,28.4 -152.5,76.4l11.7,19.4a181.3,181.3 0,0 1,56.1 -24.7c5.5,0 4.7,7.3 0,25.2l-27.3,112.2c-16.6,64 0,77.7 24.5,77.7s85.3,-21.3 141,-77.7l-13.4,-17.9a122.7,122.7 0,0 1,-53.1 26.9z" />
</vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM13,14h-2v-2h2v2zM13,10h-2L11,6h2v4z"/>
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M864,192L704,192L704,96c0,-17.7 -14.3,-32 -32,-32L352,64c-9,0 -17.2,3.7 -22.9,9.7L137.7,265.1c-6,5.8 -9.7,14 -9.7,22.9v512c0,17.7 14.3,32 32,32h160v96c0,17.7 14.3,32 32,32h512c17.7,0 32,-14.3 32,-32L896,224c0,-17.7 -14.3,-32 -32,-32zM320,173.2L320,256h-82.8l82.8,-82.8zM192,768L192,320h160c17.7,0 32,-14.3 32,-32L384,128h256v64h-96c-9,0 -17.2,3.7 -22.9,9.7L329.7,393.1c-6,5.8 -9.7,14 -9.7,22.9v352L192,768zM512,301.2L512,384h-82.8l82.8,-82.8zM832,896L384,896L384,448h160c17.7,0 32,-14.3 32,-32L576,256h256v640z" />
</vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M20,8L4,8L4,6h16v2zM18,2L6,2v2h12L18,2zM22,12v8c0,1.1 -0.9,2 -2,2L4,22c-1.1,0 -2,-0.9 -2,-2v-8c0,-1.1 0.9,-2 2,-2h16c1.1,0 2,0.9 2,2zM16,16l-6,-3.27v6.53L16,16z"/>
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M512,192.7v42.7a21.3,21.3 0,0 1,-21.3 21.3H213.5V770.6l552.9,-2.2v-233.4a21.3,21.3 0,0 1,21.3 -21.3h42.7a21.3,21.3 0,0 1,21.3 21.3v256.1c0,32.6 -24.9,59.3 -56.6,62.4l-6.1,0.3 -598.2,2.1c-32.6,0 -59.3,-24.8 -62.4,-56.6l-0.3,-6V234c0,-32.6 24.8,-59.3 56.6,-62.4l6,-0.3H490.7a21.3,21.3 0,0 1,21.3 21.3z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M848.7,238.2l-250.8,250.8a21.3,21.3 0,0 1,-13.1 6.1c-30.8,2.9 -47.9,2.6 -51.2,-0.8 -3.4,-3.4 -4,-20.8 -2,-52.3a21.3,21.3 0,0 1,6.2 -13.7l250.5,-250.6a21.3,21.3 0,0 1,30.2 0l30.2,30.2a21.3,21.3 0,0 1,0 30.2z" />
</vector>

View File

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

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M784,112L240,112c-88,0 -160,72 -160,160v480c0,88 72,160 160,160h544c88,0 160,-72 160,-160L944,272c0,-88 -72,-160 -160,-160zM880,752c0,52.8 -43.2,96 -96,96L240,848c-52.8,0 -96,-43.2 -96,-96L144,272c0,-52.8 43.2,-96 96,-96h544c52.8,0 96,43.2 96,96v480z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M352,480c52.8,0 96,-43.2 96,-96s-43.2,-96 -96,-96 -96,43.2 -96,96 43.2,96 96,96zM352,352c17.6,0 32,14.4 32,32s-14.4,32 -32,32 -32,-14.4 -32,-32 14.4,-32 32,-32zM814.4,731.2l-3.2,-3.2 -177.6,-177.6c-25.6,-25.6 -65.6,-25.6 -91.2,0l-80,80 -36.8,-36.8c-25.6,-25.6 -65.6,-25.6 -91.2,0L200,728c-4.8,6.4 -8,14.4 -8,24 0,17.6 14.4,32 32,32 9.6,0 16,-3.2 22.4,-9.6L380.8,640l134.4,134.4c6.4,6.4 14.4,9.6 24,9.6 17.6,0 32,-14.4 32,-32 0,-9.6 -4.8,-17.6 -9.6,-24l-52.8,-52.8 80,-80L769.6,776c6.4,4.8 12.8,8 20.8,8 17.6,0 32,-14.4 32,-32 0,-8 -3.2,-16 -8,-20.8z" />
</vector>

View File

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

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M213.333333 789.461333V234.538667C213.333333 168.533333 285.013333 127.530667 341.930667 160.981333l471.68 277.461334c56.106667 32.981333 56.106667 114.133333 0 147.114666L341.930667 863.018667C285.056 896.469333 213.333333 855.466667 213.333333 789.461333z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M471.5,77.5a128,128 0,0 1,72.1 -2.6l8.9,2.6 256,85.3a128,128 0,0 1,87.3 113.6l0.3,7.8L896,512c0,101.6 -44.5,186 -103.2,253.1l-8.1,9 -12.1,12.8a618.2,618.2 0,0 1,-25 24.2l-12.8,11.4 -13,11a839.3,839.3 0,0 1,-127.7 86.5l-19.5,10.5 -17.5,9a101.4,101.4 0,0 1,-90.5 0l-18.9,-9.6c-40.1,-21.1 -93.9,-53.2 -145.9,-96.3l-12.9,-11 -12.8,-11.4a618.2,618.2 0,0 1,-24.9 -24.2l-12.1,-12.8 -8.1,-9c-55.9,-63.9 -98.9,-143.4 -102.9,-238.6L128,512L128,284.2A128,128 0,0 1,208.2 165.5l7.3,-2.7 256,-85.3zM512,661.3c-80.2,0 -151.1,40.3 -193.4,101.7 62,57.2 132.2,97.2 176.6,119a37.4,37.4 0,0 0,33.7 0c44.4,-21.9 114.6,-61.9 176.6,-119A234.4,234.4 0,0 0,512 661.3zM491.8,138.2l-256,85.3A64,64 0,0 0,192 284.2L192,512c0,78.5 32.9,146.4 81.5,204.2A298.2,298.2 0,0 1,512 597.3a298.2,298.2 0,0 1,238.5 118.8C799.1,658.4 832,590.5 832,512L832,284.2a64,64 0,0 0,-43.8 -60.7l-256,-85.3a64,64 0,0 0,-40.4 0zM512,298.7a128,128 0,1 1,0 256,128 128,0 0,1 0,-256zM512,362.7a64,64 0,1 0,0 128,64 64,0 0,0 0,-128z" />
</vector>

View File

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

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M938.7,512a384,384 0,0 1,-384 384,379.3 379.3,0 0,1 -220.2,-69.5 21.8,21.8 0,0 1,-9 -15.8,21.3 21.3,0 0,1 6,-16.6l30.7,-31.1a21.3,21.3 0,0 1,26.9 -2.6A294.8,294.8 0,0 0,554.7 810.7a298.7,298.7 0,1 0,-298.7 -298.7h100.7a20.9,20.9 0,0 1,15.4 6.4l8.5,8.5a21.3,21.3 0,0 1,0 30.3L230,708.3a21.8,21.8 0,0 1,-30.3 0l-150.6,-151a21.3,21.3 0,0 1,0 -30.3l8.5,-8.5a20.9,20.9 0,0 1,15.4 -6.4H170.7a384,384 0,0 1,768 0z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M181.333333 384a32 32 0 0 1-64 0v-111.146667a155.52 155.52 0 0 1 155.52-155.52H384a32 32 0 0 1 0 64h-111.146667a91.52 91.52 0 0 0-91.52 91.52V384zM640 181.333333a32 32 0 0 1 0-64h111.146667a155.52 155.52 0 0 1 155.52 155.52V384a32 32 0 0 1-64 0v-111.146667a91.52 91.52 0 0 0-91.52-91.52H640zM842.666667 640a32 32 0 0 1 64 0v111.146667a155.52 155.52 0 0 1-155.52 155.52H640a32 32 0 0 1 0-64h111.146667a91.52 91.52 0 0 0 91.52-91.52V640zM384 842.666667a32 32 0 0 1 0 64h-111.146667a155.52 155.52 0 0 1-155.52-155.52V640a32 32 0 0 1 64 0v111.146667a91.52 91.52 0 0 0 91.52 91.52H384z m-192-298.666667a32 32 0 0 1 0-64h640a32 32 0 0 1 0 64H192z" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M924.8,625.7l-65.5,-56c3.1,-19 4.7,-38.4 4.7,-57.8s-1.6,-38.8 -4.7,-57.8l65.5,-56c10.1,-8.6 13.8,-22.6 9.3,-35.2l-0.9,-2.6c-18.1,-50.5 -44.9,-96.9 -79.7,-137.9l-1.8,-2.1c-8.6,-10.1 -22.5,-13.9 -35.1,-9.5l-81.3,28.9c-30,-24.6 -63.5,-44 -99.7,-57.6l-15.7,-85c-2.4,-13.1 -12.7,-23.3 -25.8,-25.7l-2.7,-0.5c-52.1,-9.4 -106.9,-9.4 -159,0l-2.7,0.5c-13.1,2.4 -23.4,12.6 -25.8,25.7l-15.8,85.4c-35.9,13.6 -69.2,32.9 -99,57.4l-81.9,-29.1c-12.5,-4.4 -26.5,-0.7 -35.1,9.5l-1.8,2.1c-34.8,41.1 -61.6,87.5 -79.7,137.9l-0.9,2.6c-4.5,12.5 -0.8,26.5 9.3,35.2l66.3,56.6c-3.1,18.8 -4.6,38 -4.6,57.1 0,19.2 1.5,38.4 4.6,57.1L99,625.5c-10.1,8.6 -13.8,22.6 -9.3,35.2l0.9,2.6c18.1,50.4 44.9,96.9 79.7,137.9l1.8,2.1c8.6,10.1 22.5,13.9 35.1,9.5l81.9,-29.1c29.8,24.5 63.1,43.9 99,57.4l15.8,85.4c2.4,13.1 12.7,23.3 25.8,25.7l2.7,0.5c26.1,4.7 52.8,7.1 79.5,7.1 26.7,0 53.5,-2.4 79.5,-7.1l2.7,-0.5c13.1,-2.4 23.4,-12.6 25.8,-25.7l15.7,-85c36.2,-13.6 69.7,-32.9 99.7,-57.6l81.3,28.9c12.5,4.4 26.5,0.7 35.1,-9.5l1.8,-2.1c34.8,-41.1 61.6,-87.5 79.7,-137.9l0.9,-2.6c4.5,-12.3 0.8,-26.3 -9.3,-35zM788.3,465.9c2.5,15.1 3.8,30.6 3.8,46.1s-1.3,31 -3.8,46.1l-6.6,40.1 74.7,63.9c-11.3,26.1 -25.6,50.7 -42.6,73.6L721,702.8l-31.4,25.8c-23.9,19.6 -50.5,35 -79.3,45.8l-38.1,14.3 -17.9,97c-28.1,3.2 -56.8,3.2 -85,0l-17.9,-97.2 -37.8,-14.5c-28.5,-10.8 -55,-26.2 -78.7,-45.7l-31.4,-25.9 -93.4,33.2c-17,-22.9 -31.2,-47.6 -42.6,-73.6l75.5,-64.5 -6.5,-40c-2.4,-14.9 -3.7,-30.3 -3.7,-45.5 0,-15.3 1.2,-30.6 3.7,-45.5l6.5,-40 -75.5,-64.5c11.3,-26.1 25.6,-50.7 42.6,-73.6l93.4,33.2 31.4,-25.9c23.7,-19.5 50.2,-34.9 78.7,-45.7l37.9,-14.3 17.9,-97.2c28.1,-3.2 56.8,-3.2 85,0l17.9,97 38.1,14.3c28.7,10.8 55.4,26.2 79.3,45.8l31.4,25.8 92.8,-32.9c17,22.9 31.2,47.6 42.6,73.6L781.8,426l6.5,39.9z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M512,326c-97.2,0 -176,78.8 -176,176s78.8,176 176,176 176,-78.8 176,-176 -78.8,-176 -176,-176zM591.2,581.2C570,602.3 541.9,614 512,614c-29.9,0 -58,-11.7 -79.2,-32.8C411.7,560 400,531.9 400,502c0,-29.9 11.7,-58 32.8,-79.2C454,401.6 482.1,390 512,390c29.9,0 58,11.6 79.2,32.8C612.3,444 624,472.1 624,502c0,29.9 -11.7,58 -32.8,79.2z" />
</vector>

View File

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

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M256 192h512c35.392 0 64 28.608 64 64v512c0 35.392-28.608 64-64 64H256c-35.392 0-64-28.608-64-64V256c0-35.392 28.608-64 64-64z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M170.6,256h682.7v85.3L170.6,341.3L170.6,256zM256,85.3h512v85.4L256,170.7L256,85.3zM853.3,426.7L170.6,426.7c-46.9,0 -85.3,38.4 -85.3,85.3v341.3c0,47 38.4,85.4 85.3,85.4h682.7c46.9,0 85.3,-38.4 85.3,-85.4L938.6,512c0,-46.9 -38.4,-85.3 -85.3,-85.3zM853.3,853.3L170.6,853.3L170.6,512h682.7v341.3z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M912.2,154A85.3,85.3 0,0 0,853.3 128a85.3,85.3 0,0 0,-33.3 6.8l-708.3,306.8a42.7,42.7 0,0 0,-26.5 42.7V512a42.7,42.7 0,0 0,29.4 42.7L298.7,616.1l55.5,187.3a75.9,75.9 0,0 0,56.3 53.3,62.7 62.7,0 0,0 15.4,0 73.8,73.8 0,0 0,50.8 -20.5l67.8,-64 131.4,103.7a85.3,85.3 0,0 0,90 9.4l14.1,-7.3a88.3,88.3 0,0 0,46.5 -62.7L938.7,235.1a90,90 0,0 0,-26.5 -81.1zM763.7,805.1a25.2,25.2 0,0 1,-12.8 17.5l-14.1,7.3a19.6,19.6 0,0 1,-9 2.1,19.6 19.6,0 0,1 -12.4,-4.7l-160.4,-128a20.9,20.9 0,0 0,-27.7 0l-94.7,89.2a11.1,11.1 0,0 1,-6 2.1V640a21.8,21.8 0,0 1,6.8 -15.8c136.1,-128 217.6,-199.7 266.2,-240.6a15.8,15.8 0,0 0,5.1 -11.1,13.7 13.7,0 0,0 -4.3,-11.1 14.9,14.9 0,0 0,-17.9 -4.3l-322.6,203.5a21.3,21.3 0,0 1,-18.3 0L149.3,494.9l694.2,-301.2a16.6,16.6 0,0 1,7.7 0,22.2 22.2,0 0,1 16.2,7.7 26.9,26.9 0,0 1,6.8 23.5z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

View File

@@ -1,9 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="135"
android:centerColor="#009688"
android:endColor="#00695C"
android:startColor="#4DB6AC"
android:type="linear" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FF000000"
android:pathData="M810.7,206.3A426.7,426.7 0,1 0,512 938.7h5.5A426.7,426.7 0,0 0,810.7 206.3zM771.8,765A360.1,360.1 0,0 1,517.1 874.7L512,874.7a362.7,362.7 0,0 1,-4.9 -725.3h3.4a362.7,362.7 0,0 1,261.3 615.7z" />
<path
android:fillColor="#FF000000"
android:pathData="M547.2,390a57.4,57.4 0,0 0,62.3 -55.3,39.3 39.3,0 0,0 -45,-42.7 58.9,58.9 0,0 0,-64 54.6c-1.7,26.9 13.9,43.3 46.7,43.3zM548.3,663.5c-5.5,0 -7.9,-7.3 -2.3,-28.2l31.1,-118c11.7,-42.7 7.9,-71.3 -15.8,-71.3 -28.4,0 -94.7,28.4 -152.5,76.4l11.7,19.4a181.3,181.3 0,0 1,56.1 -24.7c5.5,0 4.7,7.3 0,25.2l-27.3,112.2c-16.6,64 0,77.7 24.5,77.7s85.3,-21.3 141,-77.7l-13.4,-17.9a122.7,122.7 0,0 1,-53.1 26.9z" />
</vector>

View File

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

View File

@@ -5,5 +5,5 @@
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,8L4,8L4,6h16v2zM18,2L6,2v2h12L18,2zM22,12v8c0,1.1 -0.9,2 -2,2L4,22c-1.1,0 -2,-0.9 -2,-2v-8c0,-1.1 0.9,-2 2,-2h16c1.1,0 2,0.9 2,2zM16,16l-6,-3.27v6.53L16,16z"/>
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

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